mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Qwikswitch binary sensors (#14008)
This commit is contained in:
parent
2a5fac3b9d
commit
6ccb83584e
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal file
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal 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
|
@ -8,17 +8,18 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||||
|
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL,
|
CONF_SENSORS, CONF_SWITCHES, CONF_URL, EVENT_HOMEASSISTANT_START,
|
||||||
CONF_SENSORS, CONF_SWITCHES)
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
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.discovery import load_platform
|
||||||
from homeassistant.helpers.entity import Entity
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ CONF_DIMMER_ADJUST = 'dimmer_adjust'
|
|||||||
CONF_BUTTON_EVENTS = 'button_events'
|
CONF_BUTTON_EVENTS = 'button_events'
|
||||||
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
|
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Required(CONF_URL, default='http://127.0.0.1:2020'):
|
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.Optional('channel', default=1): int,
|
||||||
vol.Required('name'): str,
|
vol.Required('name'): str,
|
||||||
vol.Required('type'): str,
|
vol.Required('type'): str,
|
||||||
|
vol.Optional('class'): DEVICE_CLASSES_SCHEMA,
|
||||||
|
vol.Optional('invert'): bool
|
||||||
})]),
|
})]),
|
||||||
vol.Optional(CONF_SWITCHES, default=[]): vol.All(
|
vol.Optional(CONF_SWITCHES, default=[]): vol.All(
|
||||||
cv.ensure_list, [str])
|
cv.ensure_list, [str])
|
||||||
@ -115,7 +119,7 @@ class QSToggleEntity(QSEntity):
|
|||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Qwiskswitch component setup."""
|
"""Qwiskswitch component setup."""
|
||||||
from pyqwikswitch.async_ import QSUsb
|
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
|
# Add cmd's to in /&listen packets will fire events
|
||||||
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
||||||
@ -143,22 +147,39 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
hass.data[DOMAIN] = qsusb
|
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():
|
for qsid, dev in qsusb.devices.items():
|
||||||
if qsid in switches:
|
if qsid in switches:
|
||||||
if dev.qstype != QSType.relay:
|
if dev.qstype != QSType.relay:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"You specified a switch that is not a relay %s", qsid)
|
"You specified a switch that is not a relay %s", qsid)
|
||||||
continue
|
continue
|
||||||
_new['switch'].append(qsid)
|
comps['switch'].append(qsid)
|
||||||
elif dev.qstype in (QSType.relay, QSType.dimmer):
|
elif dev.qstype in (QSType.relay, QSType.dimmer):
|
||||||
_new['light'].append(qsid)
|
comps['light'].append(qsid)
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
|
_LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Load platforms
|
# Load platforms
|
||||||
for comp_name, comp_conf in _new.items():
|
for comp_name, comp_conf in comps.items():
|
||||||
if comp_conf:
|
if comp_conf:
|
||||||
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)
|
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)
|
||||||
|
|
||||||
@ -190,9 +211,8 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_stop(_):
|
def async_stop(_):
|
||||||
"""Stop the listener queue and clean up."""
|
"""Stop the listener."""
|
||||||
hass.data[DOMAIN].stop()
|
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)
|
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop)
|
||||||
|
|
||||||
|
@ -36,18 +36,18 @@ class QSSensor(QSEntity):
|
|||||||
|
|
||||||
super().__init__(sensor['id'], sensor['name'])
|
super().__init__(sensor['id'], sensor['name'])
|
||||||
self.channel = sensor['channel']
|
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):
|
if isinstance(self.unit, type):
|
||||||
self.unit = "{}:{}".format(self.sensor_type, self.channel)
|
self.unit = "{}:{}".format(sensor_type, self.channel)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_packet(self, packet):
|
def update_packet(self, packet):
|
||||||
"""Receive update packet from QSUSB."""
|
"""Receive update packet from QSUSB."""
|
||||||
val = self._decode(packet.get('data'), channel=self.channel)
|
val = self._decode(packet, channel=self.channel)
|
||||||
_LOGGER.debug("Update %s (%s) decoded as %s: %s: %s",
|
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
|
||||||
self.entity_id, self.qsid, val, self.channel, packet)
|
self.entity_id, self.qsid, self.channel, val, packet)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
self._val = val
|
self._val = val
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
@ -898,7 +898,7 @@ pyowm==2.8.0
|
|||||||
pypollencom==1.1.2
|
pypollencom==1.1.2
|
||||||
|
|
||||||
# homeassistant.components.qwikswitch
|
# homeassistant.components.qwikswitch
|
||||||
pyqwikswitch==0.71
|
pyqwikswitch==0.8
|
||||||
|
|
||||||
# homeassistant.components.rainbird
|
# homeassistant.components.rainbird
|
||||||
pyrainbird==0.1.3
|
pyrainbird==0.1.3
|
||||||
|
@ -149,7 +149,7 @@ pymonoprice==0.3
|
|||||||
pynx584==0.4
|
pynx584==0.4
|
||||||
|
|
||||||
# homeassistant.components.qwikswitch
|
# homeassistant.components.qwikswitch
|
||||||
pyqwikswitch==0.71
|
pyqwikswitch==0.8
|
||||||
|
|
||||||
# homeassistant.components.sensor.darksky
|
# homeassistant.components.sensor.darksky
|
||||||
# homeassistant.components.weather.darksky
|
# homeassistant.components.weather.darksky
|
||||||
|
@ -13,17 +13,19 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class AiohttpClientMockResponseList(list):
|
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, _):
|
def decode(self, _):
|
||||||
"""Return next item from list."""
|
"""Return next item from list."""
|
||||||
try:
|
try:
|
||||||
res = list.pop(self)
|
res = list.pop(self, 0)
|
||||||
_LOGGER.debug("MockResponseList popped %s: %s", res, self)
|
_LOGGER.debug("MockResponseList popped %s: %s", res, self)
|
||||||
return res
|
return res
|
||||||
except IndexError:
|
except IndexError:
|
||||||
_LOGGER.debug("MockResponseList empty")
|
raise AssertionError("MockResponseList empty")
|
||||||
return ""
|
|
||||||
|
|
||||||
async def wait_till_empty(self, hass):
|
async def wait_till_empty(self, hass):
|
||||||
"""Wait until empty."""
|
"""Wait until empty."""
|
||||||
@ -52,8 +54,8 @@ def aioclient_mock():
|
|||||||
yield mock_session
|
yield mock_session
|
||||||
|
|
||||||
|
|
||||||
async def test_sensor_device(hass, aioclient_mock):
|
async def test_binary_sensor_device(hass, aioclient_mock):
|
||||||
"""Test a sensor device."""
|
"""Test a binary sensor device."""
|
||||||
config = {
|
config = {
|
||||||
'qwikswitch': {
|
'qwikswitch': {
|
||||||
'sensors': {
|
'sensors': {
|
||||||
@ -67,21 +69,49 @@ async def test_sensor_device(hass, aioclient_mock):
|
|||||||
await async_setup_component(hass, QWIKSWITCH, config)
|
await async_setup_component(hass, QWIKSWITCH, config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state_obj = hass.states.get('sensor.s1')
|
state_obj = hass.states.get('binary_sensor.s1')
|
||||||
assert state_obj
|
assert state_obj.state == 'off'
|
||||||
assert state_obj.state == 'None'
|
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
|
||||||
LISTEN.append( # Close
|
LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}')
|
||||||
"""{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""")
|
LISTEN.append('') # Will cause a sleep
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state_obj = hass.states.get('sensor.s1')
|
state_obj = hass.states.get('binary_sensor.s1')
|
||||||
assert state_obj.state == 'True'
|
assert state_obj.state == 'on'
|
||||||
|
|
||||||
# Causes a 30second delay: can be uncommented when upstream library
|
LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}')
|
||||||
# allows cancellation of asyncio.sleep(30) on failed packet ("")
|
hass.data[QWIKSWITCH]._sleep_task.cancel()
|
||||||
# LISTEN.append( # Open
|
await LISTEN.wait_till_empty(hass)
|
||||||
# """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""")
|
state_obj = hass.states.get('binary_sensor.s1')
|
||||||
# await LISTEN.wait_till_empty(hass)
|
assert state_obj.state == 'off'
|
||||||
# state_obj = hass.states.get('sensor.s1')
|
|
||||||
# assert state_obj.state == 'False'
|
|
||||||
|
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…
x
Reference in New Issue
Block a user