Qwikswitch binary sensors (#14008)
parent
2a5fac3b9d
commit
6ccb83584e
homeassistant/components
tests/components
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
Loading…
Reference in New Issue