mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
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
@ -4,6 +4,7 @@ from typing import Any, Coroutine, List, Optional
|
|||||||
|
|
||||||
import zigpy.exceptions
|
import zigpy.exceptions
|
||||||
import zigpy.zcl.clusters.general as general
|
import zigpy.zcl.clusters.general as general
|
||||||
|
from zigpy.zcl.foundation import Status
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
@ -19,6 +20,7 @@ from ..const import (
|
|||||||
SIGNAL_SET_LEVEL,
|
SIGNAL_SET_LEVEL,
|
||||||
SIGNAL_UPDATE_DEVICE,
|
SIGNAL_UPDATE_DEVICE,
|
||||||
)
|
)
|
||||||
|
from ..helpers import retryable_req
|
||||||
from .base import ClientChannel, ZigbeeChannel, parse_and_log_command
|
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}]
|
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)
|
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id)
|
||||||
class AnalogOutput(ZigbeeChannel):
|
class AnalogOutput(ZigbeeChannel):
|
||||||
"""Analog Output channel."""
|
"""Analog Output channel."""
|
||||||
|
|
||||||
REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}]
|
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)
|
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id)
|
||||||
class AnalogValue(ZigbeeChannel):
|
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.fan import DOMAIN as FAN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT
|
from homeassistant.components.light import DOMAIN as LIGHT
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK
|
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.sensor import DOMAIN as SENSOR
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ BINDINGS = "bindings"
|
|||||||
|
|
||||||
CHANNEL_ACCELEROMETER = "accelerometer"
|
CHANNEL_ACCELEROMETER = "accelerometer"
|
||||||
CHANNEL_ANALOG_INPUT = "analog_input"
|
CHANNEL_ANALOG_INPUT = "analog_input"
|
||||||
|
CHANNEL_ANALOG_OUTPUT = "analog_output"
|
||||||
CHANNEL_ATTRIBUTE = "attribute"
|
CHANNEL_ATTRIBUTE = "attribute"
|
||||||
CHANNEL_BASIC = "basic"
|
CHANNEL_BASIC = "basic"
|
||||||
CHANNEL_COLOR = "light_color"
|
CHANNEL_COLOR = "light_color"
|
||||||
@ -110,6 +112,7 @@ COMPONENTS = (
|
|||||||
FAN,
|
FAN,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
LOCK,
|
LOCK,
|
||||||
|
NUMBER,
|
||||||
SENSOR,
|
SENSOR,
|
||||||
SWITCH,
|
SWITCH,
|
||||||
)
|
)
|
||||||
|
@ -22,6 +22,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import,
|
|||||||
fan,
|
fan,
|
||||||
light,
|
light,
|
||||||
lock,
|
lock,
|
||||||
|
number,
|
||||||
sensor,
|
sensor,
|
||||||
switch,
|
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.fan import DOMAIN as FAN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT
|
from homeassistant.components.light import DOMAIN as LIGHT
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK
|
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.sensor import DOMAIN as SENSOR
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
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.DoorLock.cluster_id: LOCK,
|
||||||
zcl.clusters.closures.WindowCovering.cluster_id: COVER,
|
zcl.clusters.closures.WindowCovering.cluster_id: COVER,
|
||||||
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
|
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
|
||||||
|
zcl.clusters.general.AnalogOutput.cluster_id: NUMBER,
|
||||||
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
|
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
|
||||||
zcl.clusters.general.OnOff.cluster_id: SWITCH,
|
zcl.clusters.general.OnOff.cluster_id: SWITCH,
|
||||||
zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR,
|
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_2",
|
||||||
"sensor.digi_xbee3_77665544_analog_input_3",
|
"sensor.digi_xbee3_77665544_analog_input_3",
|
||||||
"sensor.digi_xbee3_77665544_analog_input_4",
|
"sensor.digi_xbee3_77665544_analog_input_4",
|
||||||
|
"number.digi_xbee3_77665544_analog_output",
|
||||||
|
"number.digi_xbee3_77665544_analog_output_2",
|
||||||
],
|
],
|
||||||
"entity_map": {
|
"entity_map": {
|
||||||
("switch", "00:11:22:33:44:55:66:77-208-6"): {
|
("switch", "00:11:22:33:44:55:66:77-208-6"): {
|
||||||
@ -3714,6 +3716,16 @@ DEVICES = [
|
|||||||
"entity_class": "AnalogInput",
|
"entity_class": "AnalogInput",
|
||||||
"entity_id": "sensor.digi_xbee3_77665544_analog_input_5",
|
"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"],
|
"event_channels": ["232:0x0008"],
|
||||||
"manufacturer": "Digi",
|
"manufacturer": "Digi",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user