From 9c62149b0048d391cac9d5f817cb997344964cf9 Mon Sep 17 00:00:00 2001 From: Colin Harrington Date: Fri, 14 Dec 2018 07:42:04 -0600 Subject: [PATCH] Adding support for Plum Lightpad (#16576) * Adding basic Plum Lightpad support - https://plumlife.com/ * Used Const values is_on is a bool * Added LightpadPowerMeter Sensor to the plum_lightpad platform * Moved to async setup, Introduced a PlumManager, events, subscription, Light and Power meter working * Added PlumMotionSensor * Added Glow Ring support * Updated plum library and re-normalized * set the glow-ring's icon * Naming the glow ring * Formatting and linting * Cleaned up a number of linting issues. Left a number of documentation warnings * setup_platform migrated to async_setup_platform Plum discovery run as a job * bumped plumlightpad version * On shutdown disconnect the telnet session from each plum lightpad * Cleanup & formatting. Worked on parallell cloud update * Moved setup from async to non-async * Utilize async_call_later from the helpers * Cleanedup and linted, down to documentation & one #TODO * Remove commented out debug lines * Fixed Linting issues * Remove TODO * Updated comments & fixed Linting issues * Added plumlightpad to requirements_all.txt * Fixing imports with isort * Added components to .coveragerc * Added PLUM_DATA constant for accessing hass.data[PLUM_DATA] * used dictionary syntax vs get(...) for config * Linting needed an additonal line * Fully async_setup now. removed @callback utilize bus events for detecting new devices found. * Upgraded to plumlightpad 0.0.10 * Removed extra unused PLATFORM_SCHEMA declarations * Moved listener attachment to `async_added_to_hass` and removed unused properties & device_state_attributes * Utilized Discovery when devices were located * Linting and cleanup * used `hass.async_create_task` instead of `hass.async_add_job` per Martin * Removed redundant criteria in if block * Without discovery info, there is no need to setup * Better state management and async on/off for Glow Ring * renamed async_set_config back to set_config, fixed cleanup callback and Plum Initialization * Fixed flake8 linting issues * plumlightpad package update * Add 'motion' device_class to Motion Sensor * Fixed last known Linting issue * let homeassistant handle setting the brightness state * String formatting vs concatenation * use shared aiohttp session from homeassistant * Updating to use new formatting style * looks like @cleanup isn't neccesary * ditch the serial awaits * Ensure async_add_entities is only called once per async_setup_platform * Creating tasks to wait for vs coroutines * Remove unused white component in the GlowRing * Used local variables for GlowRing colors & added a setter for the hs_color property to keep the values in sync * Linted and added docstring * Update the documentation path to point to the component page * Removed the extra sensor and binary_sensor platforms as requested. (To be added in later PRs) * Update plum_lightpad.py * Update plum_lightpad.py --- .coveragerc | 3 + .../components/light/plum_lightpad.py | 185 ++++++++++++++++++ homeassistant/components/plum_lightpad.py | 76 +++++++ requirements_all.txt | 3 + 4 files changed, 267 insertions(+) create mode 100644 homeassistant/components/light/plum_lightpad.py create mode 100644 homeassistant/components/plum_lightpad.py diff --git a/.coveragerc b/.coveragerc index b62c7202abd..212affd29b5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -265,6 +265,9 @@ omit = homeassistant/components/openuv/__init__.py homeassistant/components/*/openuv.py + homeassistant/components/plum_lightpad.py + homeassistant/components/*/plum_lightpad.py + homeassistant/components/pilight.py homeassistant/components/*/pilight.py diff --git a/homeassistant/components/light/plum_lightpad.py b/homeassistant/components/light/plum_lightpad.py new file mode 100644 index 00000000000..fa15c842deb --- /dev/null +++ b/homeassistant/components/light/plum_lightpad.py @@ -0,0 +1,185 @@ +""" +Support for Plum Lightpad switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.plum_lightpad/ +""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) +from homeassistant.components.plum_lightpad import PLUM_DATA +import homeassistant.util.color as color_util + +DEPENDENCIES = ['plum_lightpad'] + + +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[PLUM_DATA] + + entities = [] + + if 'lpid' in discovery_info: + lightpad = plum.get_lightpad(discovery_info['lpid']) + entities.append(GlowRing(lightpad=lightpad)) + + if 'llid' in discovery_info: + logical_load = plum.get_load(discovery_info['llid']) + entities.append(PlumLight(load=logical_load)) + + if entities: + async_add_entities(entities) + + +class PlumLight(Light): + """Represenation of a Plum Lightpad dimmer.""" + + def __init__(self, load): + """Initialize the light.""" + self._load = load + self._brightness = load.level + + async def async_added_to_hass(self): + """Subscribe to dimmerchange events.""" + self._load.add_event_listener('dimmerchange', self.dimmerchange) + + def dimmerchange(self, event): + """Change event handler updating the brightness.""" + self._brightness = event['level'] + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the switch if any.""" + return self._load.name + + @property + def brightness(self) -> int: + """Return the brightness of this switch between 0..255.""" + return self._brightness + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._brightness > 0 + + @property + def supported_features(self): + """Flag supported features.""" + if self._load.dimmable: + return SUPPORT_BRIGHTNESS + return None + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) + else: + await self._load.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._load.turn_off() + + +class GlowRing(Light): + """Represenation of a Plum Lightpad dimmer glow ring.""" + + def __init__(self, lightpad): + """Initialize the light.""" + self._lightpad = lightpad + self._name = '{} Glow Ring'.format(lightpad.friendly_name) + + self._state = lightpad.glow_enabled + self._brightness = lightpad.glow_intensity * 255.0 + + self._red = lightpad.glow_color['red'] + self._green = lightpad.glow_color['green'] + self._blue = lightpad.glow_color['blue'] + + async def async_added_to_hass(self): + """Subscribe to configchange events.""" + self._lightpad.add_event_listener('configchange', + self.configchange_event) + + def configchange_event(self, event): + """Handle Configuration change event.""" + config = event['changes'] + + self._state = config['glowEnabled'] + self._brightness = config['glowIntensity'] * 255.0 + + self._red = config['glowColor']['red'] + self._green = config['glowColor']['green'] + self._blue = config['glowColor']['blue'] + + self.schedule_update_ha_state() + + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return color_util.color_RGB_to_hs(self._red, self._green, self._blue) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the switch if any.""" + return self._name + + @property + def brightness(self) -> int: + """Return the brightness of this switch between 0..255.""" + return self._brightness + + @property + def glow_intensity(self): + """Brightness in float form.""" + return self._brightness / 255.0 + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._state + + @property + def icon(self): + """Return the crop-portait icon representing the glow ring.""" + return 'mdi:crop-portrait' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._lightpad.set_config( + {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + elif ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + red, green, blue = color_util.color_hs_to_RGB(*hs_color) + await self._lightpad.set_glow_color(red, green, blue, 0) + else: + await self._lightpad.set_config({"glowEnabled": True}) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + if ATTR_BRIGHTNESS in kwargs: + await self._lightpad.set_config( + {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + else: + await self._lightpad.set_config( + {"glowEnabled": False}) diff --git a/homeassistant/components/plum_lightpad.py b/homeassistant/components/plum_lightpad.py new file mode 100644 index 00000000000..979f257f25f --- /dev/null +++ b/homeassistant/components/plum_lightpad.py @@ -0,0 +1,76 @@ +""" +Support for Plum Lightpad switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/plum_lightpad +""" +import asyncio +import logging + +import voluptuous as vol + +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 +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['plumlightpad==0.0.11'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'plum_lightpad' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +PLUM_DATA = 'plum' + + +async def async_setup(hass, config): + """Plum Lightpad Platform initialization.""" + from plumlightpad import Plum + + conf = config[DOMAIN] + plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) + + hass.data[PLUM_DATA] = plum + + 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/requirements_all.txt b/requirements_all.txt index ba282603cd0..41a8025fee1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,6 +767,9 @@ pizzapi==0.0.3 # homeassistant.components.sensor.plex plexapi==3.0.6 +# homeassistant.components.plum_lightpad +plumlightpad==0.0.11 + # homeassistant.components.sensor.mhz19 # homeassistant.components.sensor.serial_pm pmsensor==0.4