Qwikswitch binary sensors ()

pull/14026/head
Johann Kellerman 2018-04-21 08:34:42 +02:00 committed by GitHub
parent 2a5fac3b9d
commit 6ccb83584e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 40 deletions

View File

@ -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

View File

@ -8,17 +8,18 @@ import logging
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__)
@ -28,6 +29,7 @@ CONF_DIMMER_ADJUST = 'dimmer_adjust'
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'):
@ -40,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema({
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])
@ -115,7 +119,7 @@ class QSToggleEntity(QSEntity):
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]
@ -143,22 +147,39 @@ async def async_setup(hass, config):
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)
@ -190,9 +211,8 @@ async def async_setup(hass, config):
@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)

View File

@ -36,18 +36,18 @@ class QSSensor(QSEntity):
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()

View File

@ -898,7 +898,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

View File

@ -149,7 +149,7 @@ pymonoprice==0.3
pynx584==0.4
# homeassistant.components.qwikswitch
pyqwikswitch==0.71
pyqwikswitch==0.8
# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky

View File

@ -13,17 +13,19 @@ _LOGGER = logging.getLogger(__name__)
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."""
@ -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': {
@ -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'
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'on'
# 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'
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'