Add zha AnalogOutput cluster support (#44092)
* Initial commit * black * isort * Commit suggestion from code review Co-authored-by: Alexei Chetroi <lexoid@gmail.com> * pylint * removed entity cache for present_value * fix discovery * move write_attributes to channel * write_attributes fix * write_attributes yet another fix * update_before_add=False * mains powered test device * removed test_restore_state * flake8 * removed async_configure_channel_specific * don't know what this patch does, removing * test for async_update * removed node_descriptor * fix unit_of_measurement Co-authored-by: Alexei Chetroi <lexoid@gmail.com>
This commit is contained in:
parent
38d16d3e0c
commit
0b8529a472
7 changed files with 562 additions and 0 deletions
|
@ -4,6 +4,7 @@ from typing import Any, Coroutine, List, Optional
|
|||
|
||||
import zigpy.exceptions
|
||||
import zigpy.zcl.clusters.general as general
|
||||
from zigpy.zcl.foundation import Status
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
@ -19,6 +20,7 @@ from ..const import (
|
|||
SIGNAL_SET_LEVEL,
|
||||
SIGNAL_UPDATE_DEVICE,
|
||||
)
|
||||
from ..helpers import retryable_req
|
||||
from .base import ClientChannel, ZigbeeChannel, parse_and_log_command
|
||||
|
||||
|
||||
|
@ -34,12 +36,85 @@ class AnalogInput(ZigbeeChannel):
|
|||
REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}]
|
||||
|
||||
|
||||
@registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id)
|
||||
class AnalogOutput(ZigbeeChannel):
|
||||
"""Analog Output channel."""
|
||||
|
||||
REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}]
|
||||
|
||||
@property
|
||||
def present_value(self) -> Optional[float]:
|
||||
"""Return cached value of present_value."""
|
||||
return self.cluster.get("present_value")
|
||||
|
||||
@property
|
||||
def min_present_value(self) -> Optional[float]:
|
||||
"""Return cached value of min_present_value."""
|
||||
return self.cluster.get("min_present_value")
|
||||
|
||||
@property
|
||||
def max_present_value(self) -> Optional[float]:
|
||||
"""Return cached value of max_present_value."""
|
||||
return self.cluster.get("max_present_value")
|
||||
|
||||
@property
|
||||
def resolution(self) -> Optional[float]:
|
||||
"""Return cached value of resolution."""
|
||||
return self.cluster.get("resolution")
|
||||
|
||||
@property
|
||||
def relinquish_default(self) -> Optional[float]:
|
||||
"""Return cached value of relinquish_default."""
|
||||
return self.cluster.get("relinquish_default")
|
||||
|
||||
@property
|
||||
def description(self) -> Optional[str]:
|
||||
"""Return cached value of description."""
|
||||
return self.cluster.get("description")
|
||||
|
||||
@property
|
||||
def engineering_units(self) -> Optional[int]:
|
||||
"""Return cached value of engineering_units."""
|
||||
return self.cluster.get("engineering_units")
|
||||
|
||||
@property
|
||||
def application_type(self) -> Optional[int]:
|
||||
"""Return cached value of application_type."""
|
||||
return self.cluster.get("application_type")
|
||||
|
||||
async def async_set_present_value(self, value: float) -> bool:
|
||||
"""Update present_value."""
|
||||
try:
|
||||
res = await self.cluster.write_attributes({"present_value": value})
|
||||
except zigpy.exceptions.ZigbeeException as ex:
|
||||
self.error("Could not set value: %s", ex)
|
||||
return False
|
||||
if isinstance(res, list) and all(
|
||||
[record.status == Status.SUCCESS for record in res[0]]
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@retryable_req(delays=(1, 1, 3))
|
||||
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
|
||||
"""Initialize channel."""
|
||||
return self.fetch_config(from_cache)
|
||||
|
||||
async def fetch_config(self, from_cache: bool) -> None:
|
||||
"""Get the channel configuration."""
|
||||
attributes = [
|
||||
"min_present_value",
|
||||
"max_present_value",
|
||||
"resolution",
|
||||
"relinquish_default",
|
||||
"description",
|
||||
"engineering_units",
|
||||
"application_type",
|
||||
]
|
||||
# just populates the cache, if not already done
|
||||
await self.get_attributes(attributes, from_cache=from_cache)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id)
|
||||
class AnalogValue(ZigbeeChannel):
|
||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
|||
from homeassistant.components.fan import DOMAIN as FAN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT
|
||||
from homeassistant.components.lock import DOMAIN as LOCK
|
||||
from homeassistant.components.number import DOMAIN as NUMBER
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
|
||||
|
@ -71,6 +72,7 @@ BINDINGS = "bindings"
|
|||
|
||||
CHANNEL_ACCELEROMETER = "accelerometer"
|
||||
CHANNEL_ANALOG_INPUT = "analog_input"
|
||||
CHANNEL_ANALOG_OUTPUT = "analog_output"
|
||||
CHANNEL_ATTRIBUTE = "attribute"
|
||||
CHANNEL_BASIC = "basic"
|
||||
CHANNEL_COLOR = "light_color"
|
||||
|
@ -110,6 +112,7 @@ COMPONENTS = (
|
|||
FAN,
|
||||
LIGHT,
|
||||
LOCK,
|
||||
NUMBER,
|
||||
SENSOR,
|
||||
SWITCH,
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import,
|
|||
fan,
|
||||
light,
|
||||
lock,
|
||||
number,
|
||||
sensor,
|
||||
switch,
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
|||
from homeassistant.components.fan import DOMAIN as FAN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT
|
||||
from homeassistant.components.lock import DOMAIN as LOCK
|
||||
from homeassistant.components.number import DOMAIN as NUMBER
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
|
||||
|
@ -61,6 +62,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {
|
|||
zcl.clusters.closures.DoorLock.cluster_id: LOCK,
|
||||
zcl.clusters.closures.WindowCovering.cluster_id: COVER,
|
||||
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
|
||||
zcl.clusters.general.AnalogOutput.cluster_id: NUMBER,
|
||||
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
|
||||
zcl.clusters.general.OnOff.cluster_id: SWITCH,
|
||||
zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR,
|
||||
|
|
339
homeassistant/components/zha/number.py
Normal file
339
homeassistant/components/zha/number.py
Normal file
|
@ -0,0 +1,339 @@
|
|||
"""Support for ZHA AnalogOutput cluster."""
|
||||
import functools
|
||||
import logging
|
||||
|
||||
from homeassistant.components.number import DOMAIN, NumberEntity
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_ANALOG_OUTPUT,
|
||||
DATA_ZHA,
|
||||
DATA_ZHA_DISPATCHERS,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
)
|
||||
from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN)
|
||||
|
||||
|
||||
UNITS = {
|
||||
0: "Square-meters",
|
||||
1: "Square-feet",
|
||||
2: "Milliamperes",
|
||||
3: "Amperes",
|
||||
4: "Ohms",
|
||||
5: "Volts",
|
||||
6: "Kilo-volts",
|
||||
7: "Mega-volts",
|
||||
8: "Volt-amperes",
|
||||
9: "Kilo-volt-amperes",
|
||||
10: "Mega-volt-amperes",
|
||||
11: "Volt-amperes-reactive",
|
||||
12: "Kilo-volt-amperes-reactive",
|
||||
13: "Mega-volt-amperes-reactive",
|
||||
14: "Degrees-phase",
|
||||
15: "Power-factor",
|
||||
16: "Joules",
|
||||
17: "Kilojoules",
|
||||
18: "Watt-hours",
|
||||
19: "Kilowatt-hours",
|
||||
20: "BTUs",
|
||||
21: "Therms",
|
||||
22: "Ton-hours",
|
||||
23: "Joules-per-kilogram-dry-air",
|
||||
24: "BTUs-per-pound-dry-air",
|
||||
25: "Cycles-per-hour",
|
||||
26: "Cycles-per-minute",
|
||||
27: "Hertz",
|
||||
28: "Grams-of-water-per-kilogram-dry-air",
|
||||
29: "Percent-relative-humidity",
|
||||
30: "Millimeters",
|
||||
31: "Meters",
|
||||
32: "Inches",
|
||||
33: "Feet",
|
||||
34: "Watts-per-square-foot",
|
||||
35: "Watts-per-square-meter",
|
||||
36: "Lumens",
|
||||
37: "Luxes",
|
||||
38: "Foot-candles",
|
||||
39: "Kilograms",
|
||||
40: "Pounds-mass",
|
||||
41: "Tons",
|
||||
42: "Kilograms-per-second",
|
||||
43: "Kilograms-per-minute",
|
||||
44: "Kilograms-per-hour",
|
||||
45: "Pounds-mass-per-minute",
|
||||
46: "Pounds-mass-per-hour",
|
||||
47: "Watts",
|
||||
48: "Kilowatts",
|
||||
49: "Megawatts",
|
||||
50: "BTUs-per-hour",
|
||||
51: "Horsepower",
|
||||
52: "Tons-refrigeration",
|
||||
53: "Pascals",
|
||||
54: "Kilopascals",
|
||||
55: "Bars",
|
||||
56: "Pounds-force-per-square-inch",
|
||||
57: "Centimeters-of-water",
|
||||
58: "Inches-of-water",
|
||||
59: "Millimeters-of-mercury",
|
||||
60: "Centimeters-of-mercury",
|
||||
61: "Inches-of-mercury",
|
||||
62: "°C",
|
||||
63: "°K",
|
||||
64: "°F",
|
||||
65: "Degree-days-Celsius",
|
||||
66: "Degree-days-Fahrenheit",
|
||||
67: "Years",
|
||||
68: "Months",
|
||||
69: "Weeks",
|
||||
70: "Days",
|
||||
71: "Hours",
|
||||
72: "Minutes",
|
||||
73: "Seconds",
|
||||
74: "Meters-per-second",
|
||||
75: "Kilometers-per-hour",
|
||||
76: "Feet-per-second",
|
||||
77: "Feet-per-minute",
|
||||
78: "Miles-per-hour",
|
||||
79: "Cubic-feet",
|
||||
80: "Cubic-meters",
|
||||
81: "Imperial-gallons",
|
||||
82: "Liters",
|
||||
83: "Us-gallons",
|
||||
84: "Cubic-feet-per-minute",
|
||||
85: "Cubic-meters-per-second",
|
||||
86: "Imperial-gallons-per-minute",
|
||||
87: "Liters-per-second",
|
||||
88: "Liters-per-minute",
|
||||
89: "Us-gallons-per-minute",
|
||||
90: "Degrees-angular",
|
||||
91: "Degrees-Celsius-per-hour",
|
||||
92: "Degrees-Celsius-per-minute",
|
||||
93: "Degrees-Fahrenheit-per-hour",
|
||||
94: "Degrees-Fahrenheit-per-minute",
|
||||
95: None,
|
||||
96: "Parts-per-million",
|
||||
97: "Parts-per-billion",
|
||||
98: "%",
|
||||
99: "Percent-per-second",
|
||||
100: "Per-minute",
|
||||
101: "Per-second",
|
||||
102: "Psi-per-Degree-Fahrenheit",
|
||||
103: "Radians",
|
||||
104: "Revolutions-per-minute",
|
||||
105: "Currency1",
|
||||
106: "Currency2",
|
||||
107: "Currency3",
|
||||
108: "Currency4",
|
||||
109: "Currency5",
|
||||
110: "Currency6",
|
||||
111: "Currency7",
|
||||
112: "Currency8",
|
||||
113: "Currency9",
|
||||
114: "Currency10",
|
||||
115: "Square-inches",
|
||||
116: "Square-centimeters",
|
||||
117: "BTUs-per-pound",
|
||||
118: "Centimeters",
|
||||
119: "Pounds-mass-per-second",
|
||||
120: "Delta-Degrees-Fahrenheit",
|
||||
121: "Delta-Degrees-Kelvin",
|
||||
122: "Kilohms",
|
||||
123: "Megohms",
|
||||
124: "Millivolts",
|
||||
125: "Kilojoules-per-kilogram",
|
||||
126: "Megajoules",
|
||||
127: "Joules-per-degree-Kelvin",
|
||||
128: "Joules-per-kilogram-degree-Kelvin",
|
||||
129: "Kilohertz",
|
||||
130: "Megahertz",
|
||||
131: "Per-hour",
|
||||
132: "Milliwatts",
|
||||
133: "Hectopascals",
|
||||
134: "Millibars",
|
||||
135: "Cubic-meters-per-hour",
|
||||
136: "Liters-per-hour",
|
||||
137: "Kilowatt-hours-per-square-meter",
|
||||
138: "Kilowatt-hours-per-square-foot",
|
||||
139: "Megajoules-per-square-meter",
|
||||
140: "Megajoules-per-square-foot",
|
||||
141: "Watts-per-square-meter-Degree-Kelvin",
|
||||
142: "Cubic-feet-per-second",
|
||||
143: "Percent-obscuration-per-foot",
|
||||
144: "Percent-obscuration-per-meter",
|
||||
145: "Milliohms",
|
||||
146: "Megawatt-hours",
|
||||
147: "Kilo-BTUs",
|
||||
148: "Mega-BTUs",
|
||||
149: "Kilojoules-per-kilogram-dry-air",
|
||||
150: "Megajoules-per-kilogram-dry-air",
|
||||
151: "Kilojoules-per-degree-Kelvin",
|
||||
152: "Megajoules-per-degree-Kelvin",
|
||||
153: "Newton",
|
||||
154: "Grams-per-second",
|
||||
155: "Grams-per-minute",
|
||||
156: "Tons-per-hour",
|
||||
157: "Kilo-BTUs-per-hour",
|
||||
158: "Hundredths-seconds",
|
||||
159: "Milliseconds",
|
||||
160: "Newton-meters",
|
||||
161: "Millimeters-per-second",
|
||||
162: "Millimeters-per-minute",
|
||||
163: "Meters-per-minute",
|
||||
164: "Meters-per-hour",
|
||||
165: "Cubic-meters-per-minute",
|
||||
166: "Meters-per-second-per-second",
|
||||
167: "Amperes-per-meter",
|
||||
168: "Amperes-per-square-meter",
|
||||
169: "Ampere-square-meters",
|
||||
170: "Farads",
|
||||
171: "Henrys",
|
||||
172: "Ohm-meters",
|
||||
173: "Siemens",
|
||||
174: "Siemens-per-meter",
|
||||
175: "Teslas",
|
||||
176: "Volts-per-degree-Kelvin",
|
||||
177: "Volts-per-meter",
|
||||
178: "Webers",
|
||||
179: "Candelas",
|
||||
180: "Candelas-per-square-meter",
|
||||
181: "Kelvins-per-hour",
|
||||
182: "Kelvins-per-minute",
|
||||
183: "Joule-seconds",
|
||||
185: "Square-meters-per-Newton",
|
||||
186: "Kilogram-per-cubic-meter",
|
||||
187: "Newton-seconds",
|
||||
188: "Newtons-per-meter",
|
||||
189: "Watts-per-meter-per-degree-Kelvin",
|
||||
}
|
||||
|
||||
ICONS = {
|
||||
0: "mdi:temperature-celsius",
|
||||
1: "mdi:water-percent",
|
||||
2: "mdi:gauge",
|
||||
3: "mdi:speedometer",
|
||||
4: "mdi:percent",
|
||||
5: "mdi:air-filter",
|
||||
6: "mdi:fan",
|
||||
7: "mdi:flash",
|
||||
8: "mdi:current-ac",
|
||||
9: "mdi:flash",
|
||||
10: "mdi:flash",
|
||||
11: "mdi:flash",
|
||||
12: "mdi:counter",
|
||||
13: "mdi:thermometer-lines",
|
||||
14: "mdi:timer",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Zigbee Home Automation Analog Output from config entry."""
|
||||
entities_to_create = hass.data[DATA_ZHA][DOMAIN]
|
||||
|
||||
unsub = async_dispatcher_connect(
|
||||
hass,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
functools.partial(
|
||||
discovery.async_add_entities,
|
||||
async_add_entities,
|
||||
entities_to_create,
|
||||
update_before_add=False,
|
||||
),
|
||||
)
|
||||
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_ANALOG_OUTPUT)
|
||||
class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
"""Representation of a ZHA Number entity."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
"""Init this entity."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._analog_output_channel = self.cluster_channels.get(CHANNEL_ANALOG_OUTPUT)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_accept_signal(
|
||||
self._analog_output_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return the current value."""
|
||||
return self._analog_output_channel.present_value
|
||||
|
||||
@property
|
||||
def min_value(self):
|
||||
"""Return the minimum value."""
|
||||
min_present_value = self._analog_output_channel.min_present_value
|
||||
if min_present_value is not None:
|
||||
return min_present_value
|
||||
return 0
|
||||
|
||||
@property
|
||||
def max_value(self):
|
||||
"""Return the maximum value."""
|
||||
max_present_value = self._analog_output_channel.max_present_value
|
||||
if max_present_value is not None:
|
||||
return max_present_value
|
||||
return 1023
|
||||
|
||||
@property
|
||||
def step(self):
|
||||
"""Return the value step."""
|
||||
resolution = self._analog_output_channel.resolution
|
||||
if resolution is not None:
|
||||
return resolution
|
||||
return super().step
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the number entity."""
|
||||
description = self._analog_output_channel.description
|
||||
if description is not None and len(description) > 0:
|
||||
return f"{super().name} {description}"
|
||||
return super().name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity."""
|
||||
application_type = self._analog_output_channel.application_type
|
||||
if application_type is not None:
|
||||
return ICONS.get(application_type >> 16, super().icon)
|
||||
return super().icon
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
engineering_units = self._analog_output_channel.engineering_units
|
||||
return UNITS.get(engineering_units)
|
||||
|
||||
@callback
|
||||
def async_set_state(self, attr_id, attr_name, value):
|
||||
"""Handle value update from channel."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_value(self, value):
|
||||
"""Update the current value from HA."""
|
||||
num_value = float(value)
|
||||
if await self._analog_output_channel.async_set_present_value(num_value):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Attempt to retrieve the state of the entity."""
|
||||
await super().async_update()
|
||||
_LOGGER.debug("polling current state")
|
||||
if self._analog_output_channel:
|
||||
value = await self._analog_output_channel.get_attribute_value(
|
||||
"present_value", from_cache=False
|
||||
)
|
||||
_LOGGER.debug("read value=%s", value)
|
130
tests/components/zha/test_number.py
Normal file
130
tests/components/zha/test_number.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""Test zha analog output."""
|
||||
import pytest
|
||||
import zigpy.profiles.zha
|
||||
import zigpy.types
|
||||
import zigpy.zcl.clusters.general as general
|
||||
import zigpy.zcl.foundation as zcl_f
|
||||
|
||||
from homeassistant.components.number import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import (
|
||||
async_enable_traffic,
|
||||
async_test_rejoin,
|
||||
find_entity_id,
|
||||
send_attributes_report,
|
||||
)
|
||||
|
||||
from tests.async_mock import call, patch
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def zigpy_analog_output_device(zigpy_device_mock):
|
||||
"""Zigpy analog_output device."""
|
||||
|
||||
endpoints = {
|
||||
1: {
|
||||
"device_type": zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH,
|
||||
"in_clusters": [general.AnalogOutput.cluster_id, general.Basic.cluster_id],
|
||||
"out_clusters": [],
|
||||
}
|
||||
}
|
||||
return zigpy_device_mock(endpoints)
|
||||
|
||||
|
||||
async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_device):
|
||||
"""Test zha number platform."""
|
||||
|
||||
cluster = zigpy_analog_output_device.endpoints.get(1).analog_output
|
||||
cluster.PLUGGED_ATTR_READS = {
|
||||
"present_value": 15.0,
|
||||
"max_present_value": 100.0,
|
||||
"min_present_value": 0.0,
|
||||
"relinquish_default": 50.0,
|
||||
"resolution": 1.0,
|
||||
"description": "PWM1",
|
||||
"engineering_units": 98,
|
||||
"application_type": 4 * 0x10000,
|
||||
}
|
||||
zha_device = await zha_device_joined_restored(zigpy_analog_output_device)
|
||||
# one for present_value and one for the rest configuration attributes
|
||||
assert cluster.read_attributes.call_count == 2
|
||||
assert "max_present_value" in cluster.read_attributes.call_args[0][0]
|
||||
assert "min_present_value" in cluster.read_attributes.call_args[0][0]
|
||||
assert "relinquish_default" in cluster.read_attributes.call_args[0][0]
|
||||
assert "resolution" in cluster.read_attributes.call_args[0][0]
|
||||
assert "description" in cluster.read_attributes.call_args[0][0]
|
||||
assert "engineering_units" in cluster.read_attributes.call_args[0][0]
|
||||
assert "application_type" in cluster.read_attributes.call_args[0][0]
|
||||
|
||||
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
|
||||
assert entity_id is not None
|
||||
|
||||
await async_enable_traffic(hass, [zha_device], enabled=False)
|
||||
# test that the number was created and that it is unavailable
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
# allow traffic to flow through the gateway and device
|
||||
assert cluster.read_attributes.call_count == 2
|
||||
await async_enable_traffic(hass, [zha_device])
|
||||
await hass.async_block_till_done()
|
||||
assert cluster.read_attributes.call_count == 4
|
||||
|
||||
# test that the state has changed from unavailable to 15.0
|
||||
assert hass.states.get(entity_id).state == "15.0"
|
||||
|
||||
# test attributes
|
||||
assert hass.states.get(entity_id).attributes.get("min") == 0.0
|
||||
assert hass.states.get(entity_id).attributes.get("max") == 100.0
|
||||
assert hass.states.get(entity_id).attributes.get("step") == 1.0
|
||||
assert hass.states.get(entity_id).attributes.get("icon") == "mdi:percent"
|
||||
assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%"
|
||||
assert (
|
||||
hass.states.get(entity_id).attributes.get("friendly_name")
|
||||
== "FakeManufacturer FakeModel e769900a analog_output PWM1"
|
||||
)
|
||||
|
||||
# change value from device
|
||||
assert cluster.read_attributes.call_count == 4
|
||||
await send_attributes_report(hass, cluster, {0x0055: 15})
|
||||
assert hass.states.get(entity_id).state == "15.0"
|
||||
|
||||
# update value from device
|
||||
await send_attributes_report(hass, cluster, {0x0055: 20})
|
||||
assert hass.states.get(entity_id).state == "20.0"
|
||||
|
||||
# change value from HA
|
||||
with patch(
|
||||
"zigpy.zcl.Cluster.write_attributes",
|
||||
return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
|
||||
):
|
||||
# set value via UI
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "set_value", {"entity_id": entity_id, "value": 30.0}, blocking=True
|
||||
)
|
||||
assert len(cluster.write_attributes.mock_calls) == 1
|
||||
assert cluster.write_attributes.call_args == call({"present_value": 30.0})
|
||||
cluster.PLUGGED_ATTR_READS["present_value"] = 30.0
|
||||
|
||||
# test rejoin
|
||||
assert cluster.read_attributes.call_count == 4
|
||||
await async_test_rejoin(hass, zigpy_analog_output_device, [cluster], (1,))
|
||||
assert hass.states.get(entity_id).state == "30.0"
|
||||
assert cluster.read_attributes.call_count == 6
|
||||
|
||||
# update device value with failed attribute report
|
||||
cluster.PLUGGED_ATTR_READS["present_value"] = 40.0
|
||||
# validate the entity still contains old value
|
||||
assert hass.states.get(entity_id).state == "30.0"
|
||||
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
"homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
|
||||
)
|
||||
assert hass.states.get(entity_id).state == "40.0"
|
||||
assert cluster.read_attributes.call_count == 7
|
||||
assert "present_value" in cluster.read_attributes.call_args[0][0]
|
|
@ -3612,6 +3612,8 @@ DEVICES = [
|
|||
"sensor.digi_xbee3_77665544_analog_input_2",
|
||||
"sensor.digi_xbee3_77665544_analog_input_3",
|
||||
"sensor.digi_xbee3_77665544_analog_input_4",
|
||||
"number.digi_xbee3_77665544_analog_output",
|
||||
"number.digi_xbee3_77665544_analog_output_2",
|
||||
],
|
||||
"entity_map": {
|
||||
("switch", "00:11:22:33:44:55:66:77-208-6"): {
|
||||
|
@ -3714,6 +3716,16 @@ DEVICES = [
|
|||
"entity_class": "AnalogInput",
|
||||
"entity_id": "sensor.digi_xbee3_77665544_analog_input_5",
|
||||
},
|
||||
("number", "00:11:22:33:44:55:66:77-218-13"): {
|
||||
"channels": ["analog_output"],
|
||||
"entity_class": "ZhaNumber",
|
||||
"entity_id": "number.digi_xbee3_77665544_analog_output",
|
||||
},
|
||||
("number", "00:11:22:33:44:55:66:77-219-13"): {
|
||||
"channels": ["analog_output"],
|
||||
"entity_class": "ZhaNumber",
|
||||
"entity_id": "number.digi_xbee3_77665544_analog_output_2",
|
||||
},
|
||||
},
|
||||
"event_channels": ["232:0x0008"],
|
||||
"manufacturer": "Digi",
|
||||
|
|
Loading…
Add table
Reference in a new issue