-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Qwikswitch refactor & sensor #13509
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
Qwikswitch refactor & sensor #13509
Changes from all commits
fd94a77
7a80ff6
8dc058d
e1f2048
8a20115
bbba36d
d71382b
2ba5e4d
7e2da16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,21 +4,21 @@ | |
| 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 @@ | |
| 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 @@ def name(self): | |
| @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) | ||
|
|
||
| @asyncio.coroutine | ||
| def async_turn_off(self, **kwargs): # pylint: disable=unused-argument | ||
| """Turn the device off.""" | ||
| self._qsusb.set_value(self._id, 0) | ||
|
|
||
|
|
||
| class QSSwitch(QSToggleEntity, SwitchDevice): | ||
| """Switch based on a Qwikswitch relay module.""" | ||
| self._qsusb.set_value(self.qsid, 0) | ||
|
|
||
| pass | ||
| 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 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) | ||
|
|
||
| hass.data[DOMAIN] = {} | ||
| CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType) | ||
|
|
||
| # 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") | ||
| # Discover all devices in QSUSB | ||
| if not await qsusb.update_from_devices(): | ||
| return False | ||
|
|
||
| hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) | ||
| hass.data[DOMAIN] = qsusb | ||
|
|
||
| # Discover all devices in QSUSB | ||
| yield from qsusb.update_from_devices() | ||
| hass.data[DOMAIN]['switch'] = [] | ||
| hass.data[DOMAIN]['light'] = [] | ||
| _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)") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't blocking right?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe ther is a way to cancel such a task, but since it is only on shutdown I am not too worried
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's a asyncio task or future, you can call its cancel method. If you have written a coroutine and wrapped that in a task, you can except |
||
|
|
||
| hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) | ||
|
|
||
| return True | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do? Since we're in a callback that is called by
qsusb, shouldn'tqsusbalready know to update its devices?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The QSUSB API has two main methods, listen and devices. These really have no relationship betwen them and I simply use listen to see when there is activity and call update_from_devices.
If I have access to the event loop in listen I could potentially add it there, but then the API becomes less felxible (although only HA uses it at the moment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The aiohttp session is already passed, so passing the event loop too should work from my point of view. But I don't know this library, so you should decide what fits best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets keep it like this for now, it clearly shows what it does with the API and the API is a very close to match to the manufacturer's API