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
70 changes: 70 additions & 0 deletions homeassistant/components/binary_sensor/qwikswitch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Support for Qwikswitch Binary Sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.qwikswitch/
"""
import logging

from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH
from homeassistant.core import callback

DEPENDENCIES = [QWIKSWITCH]

_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(hass, _, add_devices, discovery_info=None):
"""Add binary sensor from the main Qwikswitch component."""
if discovery_info is None:
return

qsusb = hass.data[QWIKSWITCH]
_LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s",
qsusb, discovery_info)
devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
add_devices(devs)


class QSBinarySensor(QSEntity, BinarySensorDevice):
"""Sensor based on a Qwikswitch relay/dimmer module."""

_val = False

def __init__(self, sensor):
"""Initialize the sensor."""
from pyqwikswitch import SENSORS

super().__init__(sensor['id'], sensor['name'])
self.channel = sensor['channel']
sensor_type = sensor['type']

self._decode, _ = SENSORS[sensor_type]
self._invert = not sensor.get('invert', False)
self._class = sensor.get('class', 'door')

@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
val = self._decode(packet, channel=self.channel)
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
self.entity_id, self.qsid, self.channel, val, packet)
if val is not None:
self._val = bool(val)
self.async_schedule_update_ha_state()

@property
def is_on(self):
"""Check if device is on (non-zero)."""
return self._val == self._invert

@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return "qs{}:{}".format(self.qsid, self.channel)

@property
def device_class(self):
"""Return the class of this sensor."""
return self._class
44 changes: 32 additions & 12 deletions homeassistant/components/qwikswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@

import voluptuous as vol

from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL,
CONF_SENSORS, CONF_SWITCHES)
CONF_SENSORS, CONF_SWITCHES, CONF_URL, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
from homeassistant.components.light import ATTR_BRIGHTNESS
import homeassistant.helpers.config_validation as cv

REQUIREMENTS = ['pyqwikswitch==0.71']
REQUIREMENTS = ['pyqwikswitch==0.8']

_LOGGER = logging.getLogger(__name__)

Expand All @@ -28,6 +29,7 @@
CONF_BUTTON_EVENTS = 'button_events'
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))


CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_URL, default='http://127.0.0.1:2020'):
Expand All @@ -40,6 +42,8 @@
vol.Optional('channel', default=1): int,
vol.Required('name'): str,
vol.Required('type'): str,
vol.Optional('class'): DEVICE_CLASSES_SCHEMA,
vol.Optional('invert'): bool
})]),
vol.Optional(CONF_SWITCHES, default=[]): vol.All(
cv.ensure_list, [str])
Expand Down Expand Up @@ -115,7 +119,7 @@ async def async_turn_off(self, **_):
async def async_setup(hass, config):
"""Qwiskswitch component setup."""
from pyqwikswitch.async_ import QSUsb
from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType
from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS

# Add cmd's to in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
Expand Down Expand Up @@ -143,22 +147,39 @@ def callback_value_changed(_qsd, qsid, _val):

hass.data[DOMAIN] = qsusb

_new = {'switch': [], 'light': [], 'sensor': sensors}
comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []}

try:
for sens in sensors:
_, _type = SENSORS[sens['type']]
if _type is bool:
comps['binary_sensor'].append(sens)
continue
comps['sensor'].append(sens)
for _key in ('invert', 'class'):
if _key in sens:
_LOGGER.warning(
"%s should only be used for binary_sensors: %s",
_key, sens)

except KeyError:
_LOGGER.warning("Sensor validation failed")

for qsid, dev in qsusb.devices.items():
if qsid in switches:
if dev.qstype != QSType.relay:
_LOGGER.warning(
"You specified a switch that is not a relay %s", qsid)
continue
_new['switch'].append(qsid)
comps['switch'].append(qsid)
elif dev.qstype in (QSType.relay, QSType.dimmer):
_new['light'].append(qsid)
comps['light'].append(qsid)
else:
_LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
continue

# Load platforms
for comp_name, comp_conf in _new.items():
for comp_name, comp_conf in comps.items():
if comp_conf:
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)

Expand Down Expand Up @@ -190,9 +211,8 @@ def async_start(_):

@callback
def async_stop(_):
"""Stop the listener queue and clean up."""
"""Stop the listener."""
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)

Expand Down
12 changes: 6 additions & 6 deletions homeassistant/components/sensor/qwikswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ def __init__(self, sensor):

super().__init__(sensor['id'], sensor['name'])
self.channel = sensor['channel']
self.sensor_type = sensor['type']
sensor_type = sensor['type']

self._decode, self.unit = SENSORS[self.sensor_type]
self._decode, self.unit = SENSORS[sensor_type]
if isinstance(self.unit, type):
self.unit = "{}:{}".format(self.sensor_type, self.channel)
self.unit = "{}:{}".format(sensor_type, self.channel)

@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
val = self._decode(packet.get('data'), channel=self.channel)
_LOGGER.debug("Update %s (%s) decoded as %s: %s: %s",
self.entity_id, self.qsid, val, self.channel, packet)
val = self._decode(packet, channel=self.channel)
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
self.entity_id, self.qsid, self.channel, val, packet)
if val is not None:
self._val = val
self.async_schedule_update_ha_state()
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ pyowm==2.8.0
pypollencom==1.1.2

# homeassistant.components.qwikswitch
pyqwikswitch==0.71
pyqwikswitch==0.8

# homeassistant.components.rainbird
pyrainbird==0.1.3
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pymonoprice==0.3
pynx584==0.4

# homeassistant.components.qwikswitch
pyqwikswitch==0.71
pyqwikswitch==0.8

# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@


class AiohttpClientMockResponseList(list):
"""List that fires an event on empty pop, for aiohttp Mocker."""
"""Return multiple values for aiohttp Mocker.

aoihttp mocker uses decode to fetch the next value.
"""

def decode(self, _):
"""Return next item from list."""
try:
res = list.pop(self)
res = list.pop(self, 0)
_LOGGER.debug("MockResponseList popped %s: %s", res, self)
return res
except IndexError:
_LOGGER.debug("MockResponseList empty")
return ""
raise AssertionError("MockResponseList empty")

async def wait_till_empty(self, hass):
"""Wait until empty."""
Expand Down Expand Up @@ -52,8 +54,8 @@ def aioclient_mock():
yield mock_session


async def test_sensor_device(hass, aioclient_mock):
"""Test a sensor device."""
async def test_binary_sensor_device(hass, aioclient_mock):
"""Test a binary sensor device."""
config = {
'qwikswitch': {
'sensors': {
Expand All @@ -67,21 +69,49 @@ async def test_sensor_device(hass, aioclient_mock):
await async_setup_component(hass, QWIKSWITCH, config)
await hass.async_block_till_done()

state_obj = hass.states.get('sensor.s1')
assert state_obj
assert state_obj.state == 'None'
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'off'

hass.bus.async_fire(EVENT_HOMEASSISTANT_START)

LISTEN.append( # Close
"""{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""")
LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}')
LISTEN.append('') # Will cause a sleep
await hass.async_block_till_done()
state_obj = hass.states.get('sensor.s1')
assert state_obj.state == 'True'

# Causes a 30second delay: can be uncommented when upstream library
# allows cancellation of asyncio.sleep(30) on failed packet ("")
# LISTEN.append( # Open
# """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""")
# await LISTEN.wait_till_empty(hass)
# state_obj = hass.states.get('sensor.s1')
# assert state_obj.state == 'False'
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'on'

LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}')
hass.data[QWIKSWITCH]._sleep_task.cancel()
await LISTEN.wait_till_empty(hass)
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'off'


async def test_sensor_device(hass, aioclient_mock):
"""Test a sensor device."""
config = {
'qwikswitch': {
'sensors': {
'name': 'ss1',
'id': '@a00001',
'channel': 1,
'type': 'qwikcord',
}
}
}
await async_setup_component(hass, QWIKSWITCH, config)
await hass.async_block_till_done()

state_obj = hass.states.get('sensor.ss1')
assert state_obj.state == 'None'

hass.bus.async_fire(EVENT_HOMEASSISTANT_START)

LISTEN.append(
'{"id":"@a00001","name":"ss1","type":"rel",'
'"val":"4733800001a00000"}')
LISTEN.append('') # Will cause a sleep
await LISTEN.wait_till_empty(hass) # await hass.async_block_till_done()

state_obj = hass.states.get('sensor.ss1')
assert state_obj.state == 'None'