Support rfxtrx smoke detectors, motion sensors as binary_sensors (#38000)

* Add binary sensor support to motion sensors and smoke detectors

* Add support for new sensor events as binary sensors

Adds a default device_class for motion sensors and smoke detector

* Use device type instead of event to set class

* Add some additional binary values
This commit is contained in:
Joakim Plate 2020-07-24 17:16:41 +02:00 committed by GitHub
parent 84df0efb5e
commit 632a36d819
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 62 deletions

View File

@ -3,7 +3,11 @@ import logging
import RFXtrx as rfxtrxmod
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
DEVICE_CLASS_SMOKE,
BinarySensorEntity,
)
from homeassistant.const import (
CONF_COMMAND_OFF,
CONF_COMMAND_ON,
@ -34,6 +38,33 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
SENSOR_STATUS_ON = [
"Panic",
"Motion",
"Motion Tamper",
"Light Detected",
"Alarm",
"Alarm Tamper",
]
SENSOR_STATUS_OFF = [
"End Panic",
"No Motion",
"No Motion Tamper",
"Dark Detected",
"Normal",
"Normal Tamper",
]
DEVICE_TYPE_DEVICE_CLASS = {
"X10 Security Motion Detector": DEVICE_CLASS_MOTION,
"KD101 Smoke Detector": DEVICE_CLASS_SMOKE,
"Visonic Powercode Motion Detector": DEVICE_CLASS_MOTION,
"Alecto SA30 Smoke Detector": DEVICE_CLASS_SMOKE,
"RM174RF Smoke Detector": DEVICE_CLASS_SMOKE,
}
async def async_setup_entry(
hass, config_entry, async_add_entities,
):
@ -46,7 +77,14 @@ async def async_setup_entry(
discovery_info = config_entry.data
def supported(event):
return isinstance(event, rfxtrxmod.ControlEvent)
if isinstance(event, rfxtrxmod.ControlEvent):
return True
if isinstance(event, rfxtrxmod.SensorEvent):
return event.values.get("Sensor Status") in [
*SENSOR_STATUS_ON,
*SENSOR_STATUS_OFF,
]
return False
for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
event = get_rfx_object(packet_id)
@ -70,7 +108,10 @@ async def async_setup_entry(
device = RfxtrxBinarySensor(
event.device,
device_id,
entity_info.get(CONF_DEVICE_CLASS),
entity_info.get(
CONF_DEVICE_CLASS,
DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string),
),
entity_info.get(CONF_OFF_DELAY),
entity_info.get(CONF_DATA_BITS),
entity_info.get(CONF_COMMAND_ON),
@ -97,7 +138,12 @@ async def async_setup_entry(
event.device.subtype,
"".join(f"{x:02x}" for x in event.data),
)
sensor = RfxtrxBinarySensor(event.device, device_id, event=event)
sensor = RfxtrxBinarySensor(
event.device,
device_id,
event=event,
device_class=DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string),
)
async_add_entities([sensor])
# Subscribe to main RFXtrx events
@ -170,9 +216,13 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity):
self._state = True
def _apply_event_standard(self, event):
if event.values["Command"] in COMMAND_ON_LIST:
if event.values.get("Command") in COMMAND_ON_LIST:
self._state = True
elif event.values["Command"] in COMMAND_OFF_LIST:
elif event.values.get("Command") in COMMAND_OFF_LIST:
self._state = False
elif event.values.get("Sensor Status") in SENSOR_STATUS_ON:
self._state = True
elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF:
self._state = False
def _apply_event(self, event):
@ -200,7 +250,11 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity):
self.async_write_ha_state()
if self.is_on and self._off_delay is not None and self._delay_listener is None:
if self._delay_listener:
self._delay_listener()
self._delay_listener = None
if self.is_on and self._off_delay is not None:
@callback
def off_delay_listener(now):

View File

@ -1,16 +1,21 @@
"""Common test tools."""
from unittest import mock
from datetime import timedelta
import pytest
from homeassistant.components import rfxtrx
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.async_mock import patch
from tests.common import async_fire_time_changed
@pytest.fixture(autouse=True, name="rfxtrx")
async def rfxtrx_fixture(hass):
"""Fixture that cleans up threads from integration."""
with mock.patch("RFXtrx.Connect") as connect, mock.patch("RFXtrx.DummyTransport2"):
with patch("RFXtrx.Connect") as connect, patch("RFXtrx.DummyTransport2"):
rfx = connect.return_value
async def _signal_event(packet_id):
@ -26,3 +31,31 @@ async def rfxtrx_fixture(hass):
rfx.signal = _signal_event
yield rfx
@pytest.fixture(name="rfxtrx_automatic")
async def rfxtrx_automatic_fixture(hass, rfxtrx):
"""Fixture that starts up with automatic additions."""
assert await async_setup_component(
hass, "rfxtrx", {"rfxtrx": {"device": "abcd", "automatic_add": True}},
)
await hass.async_block_till_done()
await hass.async_start()
yield rfxtrx
@pytest.fixture
async def timestep(hass):
"""Step system time forward."""
with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
mock_utcnow.return_value = utcnow()
async def delay(seconds):
"""Trigger delay in system."""
mock_utcnow.return_value += timedelta(seconds=seconds)
async_fire_time_changed(hass, mock_utcnow.return_value)
await hass.async_block_till_done()
yield delay

View File

@ -1,14 +1,20 @@
"""The tests for the Rfxtrx sensor platform."""
from datetime import timedelta
import pytest
from homeassistant.components.rfxtrx.const import ATTR_EVENT
from homeassistant.core import State
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed, mock_restore_cache
from tests.common import mock_restore_cache
EVENT_SMOKE_DETECTOR_PANIC = "08200300a109000670"
EVENT_SMOKE_DETECTOR_NO_PANIC = "08200300a109000770"
EVENT_MOTION_DETECTOR_MOTION = "08200100a109000470"
EVENT_MOTION_DETECTOR_NO_MOTION = "08200100a109000570"
EVENT_LIGHT_DETECTOR_LIGHT = "08200100a109001570"
EVENT_LIGHT_DETECTOR_DARK = "08200100a109001470"
async def test_one(hass, rfxtrx):
@ -116,25 +122,9 @@ async def test_several(hass, rfxtrx):
assert state.attributes.get("friendly_name") == "AC 1118cdea:2"
async def test_discover(hass, rfxtrx):
async def test_discover(hass, rfxtrx_automatic):
"""Test with discovery."""
assert await async_setup_component(
hass,
"rfxtrx",
{
"rfxtrx": {
"device": "abcd",
"automatic_add": True,
"devices": {
"0b1100cd0213c7f230010f71": {},
"0b1100100118cdea02010f70": {},
"0b1100101118cdea02010f70": {},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
rfxtrx = rfxtrx_automatic
await rfxtrx.signal("0b1100100118cdea02010f70")
state = hass.states.get("binary_sensor.ac_118cdea_2")
@ -147,7 +137,7 @@ async def test_discover(hass, rfxtrx):
assert state.state == "on"
async def test_off_delay(hass, rfxtrx):
async def test_off_delay(hass, rfxtrx, timestep):
"""Test with discovery."""
assert await async_setup_component(
hass,
@ -171,16 +161,53 @@ async def test_off_delay(hass, rfxtrx):
assert state
assert state.state == "on"
base_time = utcnow()
async_fire_time_changed(hass, base_time + timedelta(seconds=4))
await hass.async_block_till_done()
await timestep(4)
state = hass.states.get("binary_sensor.ac_118cdea_2")
assert state
assert state.state == "on"
async_fire_time_changed(hass, base_time + timedelta(seconds=6))
await hass.async_block_till_done()
await timestep(4)
state = hass.states.get("binary_sensor.ac_118cdea_2")
assert state
assert state.state == "off"
async def test_panic(hass, rfxtrx_automatic):
"""Test panic entities."""
rfxtrx = rfxtrx_automatic
entity_id = "binary_sensor.kd101_smoke_detector_a10900_32"
await rfxtrx.signal(EVENT_SMOKE_DETECTOR_PANIC)
assert hass.states.get(entity_id).state == "on"
assert hass.states.get(entity_id).attributes.get("device_class") == "smoke"
await rfxtrx.signal(EVENT_SMOKE_DETECTOR_NO_PANIC)
assert hass.states.get(entity_id).state == "off"
async def test_motion(hass, rfxtrx_automatic):
"""Test motion entities."""
rfxtrx = rfxtrx_automatic
entity_id = "binary_sensor.x10_security_motion_detector_a10900_32"
await rfxtrx.signal(EVENT_MOTION_DETECTOR_MOTION)
assert hass.states.get(entity_id).state == "on"
assert hass.states.get(entity_id).attributes.get("device_class") == "motion"
await rfxtrx.signal(EVENT_MOTION_DETECTOR_NO_MOTION)
assert hass.states.get(entity_id).state == "off"
async def test_light(hass, rfxtrx_automatic):
"""Test light entities."""
rfxtrx = rfxtrx_automatic
entity_id = "binary_sensor.x10_security_motion_detector_a10900_32"
await rfxtrx.signal(EVENT_LIGHT_DETECTOR_LIGHT)
assert hass.states.get(entity_id).state == "on"
await rfxtrx.signal(EVENT_LIGHT_DETECTOR_DARK)
assert hass.states.get(entity_id).state == "off"

View File

@ -101,13 +101,9 @@ async def test_several_covers(hass, rfxtrx):
assert state.attributes.get("friendly_name") == "RollerTrol 009ba8:1"
async def test_discover_covers(hass, rfxtrx):
async def test_discover_covers(hass, rfxtrx_automatic):
"""Test with discovery of covers."""
assert await async_setup_component(
hass, "rfxtrx", {"rfxtrx": {"device": "abcd", "automatic_add": True}}
)
await hass.async_block_till_done()
await hass.async_start()
rfxtrx = rfxtrx_automatic
await rfxtrx.signal("0a140002f38cae010f0070")
state = hass.states.get("cover.lightwaverf_siemens_f38cae_1")

View File

@ -165,13 +165,9 @@ async def test_repetitions(hass, rfxtrx, repetitions):
assert rfxtrx.transport.send.call_count == repetitions
async def test_discover_light(hass, rfxtrx):
async def test_discover_light(hass, rfxtrx_automatic):
"""Test with discovery of lights."""
assert await async_setup_component(
hass, "rfxtrx", {"rfxtrx": {"device": "abcd", "automatic_add": True}},
)
await hass.async_block_till_done()
await hass.async_start()
rfxtrx = rfxtrx_automatic
await rfxtrx.signal("0b11009e00e6116202020070")
state = hass.states.get("light.ac_0e61162_2")

View File

@ -148,13 +148,9 @@ async def test_several_sensors(hass, rfxtrx):
assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE
async def test_discover_sensor(hass, rfxtrx):
async def test_discover_sensor(hass, rfxtrx_automatic):
"""Test with discovery of sensor."""
assert await async_setup_component(
hass, "rfxtrx", {"rfxtrx": {"device": "abcd", "automatic_add": True}},
)
await hass.async_block_till_done()
await hass.async_start()
rfxtrx = rfxtrx_automatic
# 1
await rfxtrx.signal("0a520801070100b81b0279")

View File

@ -120,13 +120,9 @@ async def test_repetitions(hass, rfxtrx, repetitions):
assert rfxtrx.transport.send.call_count == repetitions
async def test_discover_switch(hass, rfxtrx):
async def test_discover_switch(hass, rfxtrx_automatic):
"""Test with discovery of switches."""
assert await async_setup_component(
hass, "rfxtrx", {"rfxtrx": {"device": "abcd", "automatic_add": True}},
)
await hass.async_block_till_done()
await hass.async_start()
rfxtrx = rfxtrx_automatic
await rfxtrx.signal("0b1100100118cdea02010f70")
state = hass.states.get("switch.ac_118cdea_2")