Fix handling of attribute reports in ZHA sensors and binary sensors (#32776)

* Update sensor tests.

* Update light tests.

* Update binary_sensor tests.

* Update cover tests.

* Update device tracker tests.

* Update fan tests.

* Update lock tests.

* Update switch tests.

* add sensor attr to sensors

* add sensor attr to binary sensors

* cleanup extra var

Co-authored-by: Alexei Chetroi <alexei.chetroi@outlook.com>
This commit is contained in:
David F. Mulcahey 2020-03-13 19:17:50 -04:00 committed by Paulus Schoutsen
parent 0788bbd629
commit 706607f1d2
11 changed files with 77 additions and 104 deletions

View file

@ -64,6 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BinarySensor(ZhaEntity, BinarySensorDevice):
"""ZHA BinarySensor."""
SENSOR_ATTR = None
DEVICE_CLASS = None
def __init__(self, unique_id, zha_device, channels, **kwargs):
@ -105,6 +106,8 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
@callback
def async_set_state(self, attr_id, attr_name, value):
"""Set the state."""
if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name:
return
self._state = bool(value)
self.async_write_ha_state()
@ -121,6 +124,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
class Accelerometer(BinarySensor):
"""ZHA BinarySensor."""
SENSOR_ATTR = "acceleration"
DEVICE_CLASS = DEVICE_CLASS_MOVING
@ -128,6 +132,7 @@ class Accelerometer(BinarySensor):
class Occupancy(BinarySensor):
"""ZHA BinarySensor."""
SENSOR_ATTR = "occupancy"
DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY
@ -135,6 +140,7 @@ class Occupancy(BinarySensor):
class Opening(BinarySensor):
"""ZHA BinarySensor."""
SENSOR_ATTR = "on_off"
DEVICE_CLASS = DEVICE_CLASS_OPENING
@ -142,6 +148,8 @@ class Opening(BinarySensor):
class IASZone(BinarySensor):
"""ZHA IAS BinarySensor."""
SENSOR_ATTR = "zone_status"
async def get_device_class(self) -> None:
"""Get the HA device class from the channel."""
zone_type = await self._channel.get_attribute_value("zone_type")

View file

@ -83,6 +83,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class Sensor(ZhaEntity):
"""Base ZHA sensor."""
SENSOR_ATTR = None
_decimals = 1
_device_class = None
_divisor = 1
@ -126,6 +127,8 @@ class Sensor(ZhaEntity):
@callback
def async_set_state(self, attr_id, attr_name, value):
"""Handle state update from channel."""
if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name:
return
if value is not None:
value = self.formatter(value)
self._state = value
@ -154,6 +157,7 @@ class Sensor(ZhaEntity):
class AnalogInput(Sensor):
"""Sensor that displays analog input values."""
SENSOR_ATTR = "present_value"
pass
@ -161,6 +165,7 @@ class AnalogInput(Sensor):
class Battery(Sensor):
"""Battery sensor of power configuration cluster."""
SENSOR_ATTR = "battery_percentage_remaining"
_device_class = DEVICE_CLASS_BATTERY
_unit = UNIT_PERCENTAGE
@ -198,6 +203,7 @@ class Battery(Sensor):
class ElectricalMeasurement(Sensor):
"""Active power measurement."""
SENSOR_ATTR = "active_power"
_device_class = DEVICE_CLASS_POWER
_divisor = 10
_unit = POWER_WATT
@ -232,6 +238,7 @@ class Text(Sensor):
class Humidity(Sensor):
"""Humidity sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_HUMIDITY
_divisor = 100
_unit = UNIT_PERCENTAGE
@ -241,6 +248,7 @@ class Humidity(Sensor):
class Illuminance(Sensor):
"""Illuminance Sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_ILLUMINANCE
_unit = "lx"
@ -254,6 +262,7 @@ class Illuminance(Sensor):
class SmartEnergyMetering(Sensor):
"""Metering sensor."""
SENSOR_ATTR = "instantaneous_demand"
_device_class = DEVICE_CLASS_POWER
def formatter(self, value):
@ -270,6 +279,7 @@ class SmartEnergyMetering(Sensor):
class Pressure(Sensor):
"""Pressure sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_PRESSURE
_decimals = 0
_unit = "hPa"
@ -279,6 +289,7 @@ class Pressure(Sensor):
class Temperature(Sensor):
"""Temperature Sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_TEMPERATURE
_divisor = 100
_unit = TEMP_CELSIUS

View file

@ -102,6 +102,23 @@ def make_attribute(attrid, value, status=0):
return attr
def send_attribute_report(hass, cluster, attrid, value):
"""Send a single attribute report."""
return send_attributes_report(hass, cluster, {attrid: value})
async def send_attributes_report(hass, cluster: int, attributes: dict):
"""Cause the sensor to receive an attribute report from the network.
This is to simulate the normal device communication that happens when a
device is paired to the zigbee network.
"""
attrs = [make_attribute(attrid, value) for attrid, value in attributes.items()]
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [attrs])
await hass.async_block_till_done()
async def find_entity_id(domain, zha_device, hass):
"""Find the entity id under the testing.

View file

@ -2,7 +2,6 @@
import pytest
import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.security as security
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.binary_sensor import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
@ -11,8 +10,7 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attributes_report,
)
DEVICE_IAS = {
@ -36,17 +34,11 @@ DEVICE_OCCUPANCY = {
async def async_test_binary_sensor_on_off(hass, cluster, entity_id):
"""Test getting on and off messages for binary sensors."""
# binary sensor on
attr = make_attribute(0, 1)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2})
assert hass.states.get(entity_id).state == STATE_ON
# binary sensor off
attr.value.value = 0
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2})
assert hass.states.get(entity_id).state == STATE_OFF

View file

@ -14,8 +14,7 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attributes_report,
)
from tests.common import mock_coro
@ -64,19 +63,12 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device):
await async_enable_traffic(hass, [zha_device])
await hass.async_block_till_done()
attr = make_attribute(8, 100)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
# test that the state has changed from unavailable to off
await send_attributes_report(hass, cluster, {0: 0, 8: 100, 1: 1})
assert hass.states.get(entity_id).state == STATE_CLOSED
# test to see if it opens
attr = make_attribute(8, 0)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {0: 1, 8: 0, 1: 100})
assert hass.states.get(entity_id).state == STATE_OPEN
# close from UI

View file

@ -4,7 +4,6 @@ import time
import pytest
import zigpy.zcl.clusters.general as general
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER
from homeassistant.components.zha.core.registries import (
@ -17,8 +16,7 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attributes_report,
)
from tests.common import async_fire_time_changed
@ -66,12 +64,9 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt)
assert hass.states.get(entity_id).state == STATE_NOT_HOME
# turn state flip
attr = make_attribute(0x0020, 23)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
attr = make_attribute(0x0021, 200)
cluster.handle_message(hdr, [[attr]])
await send_attributes_report(
hass, cluster, {0x0000: 0, 0x0020: 23, 0x0021: 200, 0x0001: 2}
)
zigpy_device_dt.last_seen = time.time() + 10
next_update = dt_util.utcnow() + timedelta(seconds=30)

View file

@ -3,7 +3,6 @@ from unittest.mock import call
import pytest
import zigpy.zcl.clusters.hvac as hvac
import zigpy.zcl.foundation as zcl_f
from homeassistant.components import fan
from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED
@ -20,8 +19,7 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attributes_report,
)
@ -52,16 +50,11 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device):
assert hass.states.get(entity_id).state == STATE_OFF
# turn on at fan
attr = make_attribute(0, 1)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 2, 0: 1, 2: 3})
assert hass.states.get(entity_id).state == STATE_ON
# turn off at fan
attr.value.value = 0
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2})
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA

View file

@ -19,8 +19,7 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attributes_report,
)
from tests.common import async_fire_time_changed
@ -190,26 +189,18 @@ async def test_light(
async def async_test_on_off_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light."""
# turn on at light
attr = make_attribute(0, 1)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3})
assert hass.states.get(entity_id).state == STATE_ON
# turn off at light
attr.value.value = 0
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3})
assert hass.states.get(entity_id).state == STATE_OFF
async def async_test_on_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light."""
# turn on at light
attr = make_attribute(0, 1)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2})
assert hass.states.get(entity_id).state == STATE_ON
@ -316,10 +307,10 @@ async def async_test_level_on_off_from_hass(
async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state):
"""Test dimmer functionality from the light."""
attr = make_attribute(0, level)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(
hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22}
)
assert hass.states.get(entity_id).state == expected_state
# hass uses None for brightness of 0 in state attributes
if level == 0:

View file

@ -10,12 +10,7 @@ import zigpy.zcl.foundation as zcl_f
from homeassistant.components.lock import DOMAIN
from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED
from .common import (
async_enable_traffic,
find_entity_id,
make_attribute,
make_zcl_header,
)
from .common import async_enable_traffic, find_entity_id, send_attributes_report
from tests.common import mock_coro
@ -58,16 +53,11 @@ async def test_lock(hass, lock):
assert hass.states.get(entity_id).state == STATE_UNLOCKED
# set state to locked
attr = make_attribute(0, 1)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2})
assert hass.states.get(entity_id).state == STATE_LOCKED
# set state to unlocked
attr.value.value = 2
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 0, 0: 2, 2: 3})
assert hass.states.get(entity_id).state == STATE_UNLOCKED
# lock from HA

View file

@ -6,7 +6,6 @@ import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.homeautomation as homeautomation
import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.smartenergy as smartenergy
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.sensor import DOMAIN
import homeassistant.config as config_util
@ -28,38 +27,41 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attribute_report,
send_attributes_report,
)
async def async_test_humidity(hass, cluster, entity_id):
"""Test humidity sensor."""
await send_attribute_report(hass, cluster, 0, 1000)
await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100})
assert_state(hass, entity_id, "10.0", UNIT_PERCENTAGE)
async def async_test_temperature(hass, cluster, entity_id):
"""Test temperature sensor."""
await send_attribute_report(hass, cluster, 0, 2900)
await send_attributes_report(hass, cluster, {1: 1, 0: 2900, 2: 100})
assert_state(hass, entity_id, "29.0", "°C")
async def async_test_pressure(hass, cluster, entity_id):
"""Test pressure sensor."""
await send_attribute_report(hass, cluster, 0, 1000)
await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000})
assert_state(hass, entity_id, "1000", "hPa")
await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000})
assert_state(hass, entity_id, "1000", "hPa")
async def async_test_illuminance(hass, cluster, entity_id):
"""Test illuminance sensor."""
await send_attribute_report(hass, cluster, 0, 10)
await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20})
assert_state(hass, entity_id, "1.0", "lx")
async def async_test_metering(hass, cluster, entity_id):
"""Test metering sensor."""
await send_attribute_report(hass, cluster, 1024, 12345)
await send_attributes_report(hass, cluster, {1025: 1, 1024: 12345, 1026: 100})
assert_state(hass, entity_id, "12345.0", "unknown")
@ -73,17 +75,17 @@ async def async_test_electrical_measurement(hass, cluster, entity_id):
new_callable=mock.PropertyMock,
) as divisor_mock:
divisor_mock.return_value = 1
await send_attribute_report(hass, cluster, 1291, 100)
await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000})
assert_state(hass, entity_id, "100", "W")
await send_attribute_report(hass, cluster, 1291, 99)
await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000})
assert_state(hass, entity_id, "99", "W")
divisor_mock.return_value = 10
await send_attribute_report(hass, cluster, 1291, 1000)
await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000})
assert_state(hass, entity_id, "100", "W")
await send_attribute_report(hass, cluster, 1291, 99)
await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000})
assert_state(hass, entity_id, "9.9", "W")
@ -141,18 +143,6 @@ async def test_sensor(
await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,))
async def send_attribute_report(hass, cluster, attrid, value):
"""Cause the sensor to receive an attribute report from the network.
This is to simulate the normal device communication that happens when a
device is paired to the zigbee network.
"""
attr = make_attribute(attrid, value)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
def assert_state(hass, entity_id, state, unit_of_measurement):
"""Check that the state is what is expected.

View file

@ -12,8 +12,7 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
send_attributes_report,
)
from tests.common import mock_coro
@ -53,16 +52,11 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device):
assert hass.states.get(entity_id).state == STATE_OFF
# turn on at switch
attr = make_attribute(0, 1)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2})
assert hass.states.get(entity_id).state == STATE_ON
# turn off at switch
attr.value.value = 0
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2})
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA