Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 23 additions & 9 deletions homeassistant/components/light/qwikswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@
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
167 changes: 77 additions & 90 deletions homeassistant/components/qwikswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Copy link
Copy Markdown
Member

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't qsusb already know to update its devices?

Copy link
Copy Markdown
Member Author

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)

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member Author

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


@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)")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't blocking right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stop is not bloking, it only sets a _running varialble False so that I will exit my listen loop. Since it is a long poll it can potentiall take up to 30seconds for asyncio.Timeout - see here

Maybe ther is a way to cancel such a task, but since it is only on shutdown I am not too worried

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 CancelledError to clean up.


hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop)

return True
69 changes: 69 additions & 0 deletions homeassistant/components/sensor/qwikswitch.py
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)
22 changes: 13 additions & 9 deletions homeassistant/components/switch/qwikswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
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."""
Loading