From 3fdb0002a79abfe75f9207d7c01aff472d25bfb5 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 29 Mar 2018 23:29:46 +0200 Subject: [PATCH] Qwikswitch async refactor & sensor (#13509) --- homeassistant/components/light/qwikswitch.py | 32 +++- homeassistant/components/qwikswitch.py | 171 ++++++++---------- homeassistant/components/sensor/qwikswitch.py | 69 +++++++ homeassistant/components/switch/qwikswitch.py | 22 ++- requirements_all.txt | 2 +- 5 files changed, 185 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/sensor/qwikswitch.py diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index c4faf0f9ca0..26741525b8f 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -4,18 +4,32 @@ Support for Qwikswitch Relays and Dimmers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.qwikswitch/ """ -import logging +from homeassistant.components.qwikswitch import ( + QSToggleEntity, DOMAIN as QWIKSWITCH) +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light -DEPENDENCIES = ['qwikswitch'] +DEPENDENCIES = [QWIKSWITCH] -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, _, add_devices, discovery_info=None): """Add lights from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - "Configure Qwikswitch Light component failed") - return False + return - add_devices(hass.data['qwikswitch']['light']) - return True + qsusb = hass.data[QWIKSWITCH] + devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSLight(QSToggleEntity, Light): + """Light based on a Qwikswitch relay/dimmer module.""" + + @property + def brightness(self): + """Return the brightness of this light (0-255).""" + return self._qsusb[self.qsid, 1] if self._dim else None + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS if self._dim else 0 diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index c4901805e3e..708eff7cf11 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -4,21 +4,21 @@ Support for Qwikswitch devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ -import asyncio import logging import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL, + CONF_SENSORS, CONF_SWITCHES) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import load_platform -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.entity import Entity +from homeassistant.components.light import ATTR_BRIGHTNESS +import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.5'] +REQUIREMENTS = ['pyqwikswitch==0.6'] _LOGGER = logging.getLogger(__name__) @@ -33,11 +33,14 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_URL, default='http://127.0.0.1:2020'): vol.Coerce(str), vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE, - vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str) + vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv, + vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}), + vol.Optional(CONF_SWITCHES, default=[]): vol.All( + cv.ensure_list, [str]) })}, extra=vol.ALLOW_EXTRA) -class QSToggleEntity(object): +class QSToggleEntity(Entity): """Representation of a Qwikswitch Entity. Implement base QS methods. Modeled around HA ToggleEntity[1] & should only @@ -55,7 +58,7 @@ class QSToggleEntity(object): def __init__(self, qsid, qsusb): """Initialize the ToggleEntity.""" from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) - self._id = qsid + self.qsid = qsid self._qsusb = qsusb.devices dev = qsusb.devices[qsid] self._dim = dev[QS_TYPE] == QSType.dimmer @@ -74,129 +77,113 @@ class QSToggleEntity(object): @property def is_on(self): """Check if device is on (non-zero).""" - return self._qsusb[self._id, 1] > 0 + return self._qsusb[self.qsid, 1] > 0 - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self._id, new) + self._qsusb.set_value(self.qsid, new) - @asyncio.coroutine - def async_turn_on(self, **kwargs): - """Turn the device on.""" - new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self._id, new) - - def turn_off(self, **kwargs): # pylint: disable=unused-argument + async def async_turn_off(self, **_): """Turn the device off.""" - self._qsusb.set_value(self._id, 0) + self._qsusb.set_value(self.qsid, 0) - @asyncio.coroutine - def async_turn_off(self, **kwargs): # pylint: disable=unused-argument - """Turn the device off.""" - self._qsusb.set_value(self._id, 0) + def _update(self, _packet=None): + """Schedule an update - match dispather_send signature.""" + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self._update) -class QSSwitch(QSToggleEntity, SwitchDevice): - """Switch based on a Qwikswitch relay module.""" - - pass - - -class QSLight(QSToggleEntity, Light): - """Light based on a Qwikswitch relay/dimmer module.""" - - @property - def brightness(self): - """Return the brightness of this light (0-255).""" - return self._qsusb[self._id, 1] if self._dim else None - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS if self._dim else None - - -@asyncio.coroutine -def async_setup(hass, config): - """Setup qwiskswitch component.""" - from pyqwikswitch.async import QSUsb +async def async_setup(hass, config): + """Qwiskswitch component setup.""" + from pyqwikswitch.async_ import QSUsb from pyqwikswitch import ( - CMD_BUTTONS, QS_CMD, QSDATA, QS_ID, QS_NAME, QS_TYPE, QSType) + CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType) - hass.data[DOMAIN] = {} - - # Override which cmd's in /&listen packets will fire events + # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] - cmd_buttons = config[DOMAIN].get(CONF_BUTTON_EVENTS, ','.join(CMD_BUTTONS)) - cmd_buttons = cmd_buttons.split(',') + cmd_buttons = set(CMD_BUTTONS) + for btn in config[DOMAIN][CONF_BUTTON_EVENTS]: + cmd_buttons.add(btn) url = config[DOMAIN][CONF_URL] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] + sensors = config[DOMAIN]['sensors'] + switches = config[DOMAIN]['switches'] - def callback_value_changed(qsdevices, key, new): \ - # pylint: disable=unused-argument - """Update entiry values based on device change.""" - entity = hass.data[DOMAIN].get(key) - if entity is not None: - entity.schedule_update_ha_state() # Part of Entity/ToggleEntity + def callback_value_changed(_qsd, qsid, _val): + """Update entity values based on device change.""" + _LOGGER.debug("Dispatch %s (update from devices)", qsid) + hass.helpers.dispatcher.async_dispatcher_send(qsid, None) session = async_get_clientsession(hass) qsusb = QSUsb(url=url, dim_adj=dimmer_adjust, session=session, callback_value_changed=callback_value_changed) - @callback - def async_stop(event): # pylint: disable=unused-argument - """Stop the listener queue and clean up.""" - nonlocal qsusb - qsusb.stop() - qsusb = None - hass.data[DOMAIN] = {} - _LOGGER.info("Waiting for long poll to QSUSB to time out") - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) - # Discover all devices in QSUSB - yield from qsusb.update_from_devices() - hass.data[DOMAIN]['switch'] = [] - hass.data[DOMAIN]['light'] = [] + if not await qsusb.update_from_devices(): + return False + + hass.data[DOMAIN] = qsusb + + _new = {'switch': [], 'light': [], 'sensor': sensors} for _id, item in qsusb.devices: - if (item[QS_TYPE] == QSType.relay and - item[QSDATA][QS_NAME].lower().endswith(' switch')): - item[QSDATA][QS_NAME] = item[QSDATA][QS_NAME][:-7] # Remove switch - new_dev = QSSwitch(_id, qsusb) - hass.data[DOMAIN]['switch'].append(new_dev) + if _id in switches: + if item[QS_TYPE] != QSType.relay: + _LOGGER.warning( + "You specified a switch that is not a relay %s", _id) + continue + _new['switch'].append(_id) elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: - new_dev = QSLight(_id, qsusb) - hass.data[DOMAIN]['light'].append(new_dev) + _new['light'].append(_id) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", item) continue - hass.data[DOMAIN][_id] = new_dev # Load platforms - for comp_name in ('switch', 'light'): - if hass.data[DOMAIN][comp_name]: - load_platform(hass, comp_name, 'qwikswitch', {}, config) + for comp_name, comp_conf in _new.items(): + if comp_conf: + load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) def callback_qs_listen(item): """Typically a button press or update signal.""" - if qsusb is None: # Shutting down - return - # If button pressed, fire a hass event - if item.get(QS_CMD, '') in cmd_buttons and QS_ID in item: - hass.bus.async_fire('qwikswitch.button.{}'.format(item[QS_ID])) - return + if QS_ID in item: + if item.get(QS_CMD, '') in cmd_buttons: + hass.bus.async_fire( + 'qwikswitch.button.{}'.format(item[QS_ID]), item) + return + + # Private method due to bad __iter__ design in qsusb + # qsusb.devices returns a list of tuples + if item[QS_ID] not in \ + qsusb.devices._data: # pylint: disable=protected-access + # Not a standard device in, component can handle packet + # i.e. sensors + _LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item) + hass.helpers.dispatcher.async_dispatcher_send( + item[QS_ID], item) # Update all ha_objects hass.async_add_job(qsusb.update_from_devices) @callback - def async_start(event): # pylint: disable=unused-argument + def async_start(_): """Start listening.""" hass.async_add_job(qsusb.listen, callback_qs_listen) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) + @callback + def async_stop(_): + """Stop the listener queue and clean up.""" + hass.data[DOMAIN].stop() + _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)") + + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) + return True diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py new file mode 100644 index 00000000000..19b32e93670 --- /dev/null +++ b/homeassistant/components/sensor/qwikswitch.py @@ -0,0 +1,69 @@ +""" +Support for Qwikswitch Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.qwikswitch/ +""" +import logging + +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = [QWIKSWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, _, add_devices, discovery_info=None): + """Add lights from the main Qwikswitch component.""" + if discovery_info is None: + return + + qsusb = hass.data[QWIKSWITCH] + _LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info) + devs = [QSSensor(name, qsid) + for name, qsid in discovery_info[QWIKSWITCH].items()] + add_devices(devs) + + +class QSSensor(Entity): + """Sensor based on a Qwikswitch relay/dimmer module.""" + + _val = {} + + def __init__(self, sensor_name, sensor_id): + """Initialize the sensor.""" + self._name = sensor_name + self.qsid = sensor_id + + def update_packet(self, packet): + """Receive update packet from QSUSB.""" + _LOGGER.debug("Update %s (%s): %s", self.entity_id, self.qsid, packet) + self._val = packet + self.async_schedule_update_ha_state() + + @property + def state(self): + """Return the value of the sensor.""" + return self._val.get('data', 0) + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return self._val + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return None + + @property + def poll(self): + """QS sensors gets packets in update_packet.""" + return False + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + # Part of Entity/ToggleEntity + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self.update_packet) diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index 258e1141052..193c2722534 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -4,18 +4,22 @@ Support for Qwikswitch relays. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.qwikswitch/ """ -import logging +from homeassistant.components.qwikswitch import ( + QSToggleEntity, DOMAIN as QWIKSWITCH) +from homeassistant.components.switch import SwitchDevice -DEPENDENCIES = ['qwikswitch'] +DEPENDENCIES = [QWIKSWITCH] -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, _, add_devices, discovery_info=None): """Add switches from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - "Configure Qwikswitch Switch component failed") - return False + return - add_devices(hass.data['qwikswitch']['switch']) - return True + qsusb = hass.data[QWIKSWITCH] + devs = [QSSwitch(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSSwitch(QSToggleEntity, SwitchDevice): + """Switch based on a Qwikswitch relay module.""" diff --git a/requirements_all.txt b/requirements_all.txt index b07ed3d8fac..5b204ff621d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -860,7 +860,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.5 +pyqwikswitch==0.6 # homeassistant.components.rainbird pyrainbird==0.1.3