Skip to content

Commit 82bd715

Browse files
TheJulianJESpuddly
andauthored
Update ZHA manufacturer code handling for zigpy 0.91.x (#637)
* Fix ZHA overriding default manufacturer code * Use zigpy test helper for send_attributes_report * Expect `manufacturer=UNDEFINED` for read/write_attributes in tests --------- Co-authored-by: puddly <[email protected]>
1 parent d7c8b7b commit 82bd715

File tree

11 files changed

+144
-94
lines changed

11 files changed

+144
-94
lines changed

tests/common.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,30 +127,75 @@ def make_attribute(attrid: int, value: Any, status: int = 0) -> zcl_f.Attribute:
127127

128128

129129
async def send_attributes_report(
130-
zha_gateway: Gateway, cluster: zigpy.zcl.Cluster, attributes: dict
130+
zha_gateway: Gateway,
131+
cluster: zigpy.zcl.Cluster,
132+
attributes: dict[str | int | zcl_f.ZCLAttributeDef, Any],
133+
*,
134+
tsn: int | None = None,
131135
) -> None:
132-
"""Cause the sensor to receive an attribute report from the network.
136+
"""Mock attribute reports on a cluster."""
137+
reports = []
138+
manufacturer_codes: set[int | None] = set()
139+
140+
for attr, value in attributes.items():
141+
if isinstance(attr, int):
142+
# Raw attribute ID for unknown attributes
143+
manufacturer_codes.add(None)
144+
reports.append(
145+
zcl_f.Attribute(
146+
attrid=attr,
147+
value=zcl_f.TypeValue(
148+
type=zcl_f.DataType.from_python_type(type(value)).type_id,
149+
value=value,
150+
),
151+
)
152+
)
153+
else:
154+
attr_def = cluster.find_attribute(attr)
155+
manufacturer_codes.add(cluster._get_effective_manufacturer_code(attr_def))
133156

134-
This is to simulate the normal device communication that happens when a
135-
device is paired to the zigbee network.
136-
"""
137-
attrs = []
157+
reports.append(
158+
zcl_f.Attribute(
159+
attrid=attr_def.id,
160+
value=zcl_f.TypeValue(type=attr_def.zcl_type, value=value),
161+
)
162+
)
138163

139-
for attrid, value in attributes.items():
140-
if isinstance(attrid, str):
141-
attrid = cluster.attributes_by_name[attrid].id
142-
else:
143-
attrid = zigpy.types.uint16_t(attrid)
164+
if len(manufacturer_codes) != 1:
165+
raise ValueError(
166+
f"All attributes must have the same manufacturer code, got {manufacturer_codes}"
167+
)
144168

145-
attrs.append(make_attribute(attrid, value))
169+
if tsn is None:
170+
tsn = cluster.endpoint.device.get_sequence()
171+
172+
manufacturer: int | None = manufacturer_codes.pop()
173+
174+
frame_control = zcl_f.FrameControl(
175+
frame_type=zcl_f.FrameType.GLOBAL_COMMAND,
176+
is_manufacturer_specific=(manufacturer is not None),
177+
direction=(
178+
zcl_f.Direction.Client_to_Server
179+
if cluster.is_client
180+
else zcl_f.Direction.Server_to_Client
181+
),
182+
disable_default_response=False,
183+
reserved=0b000,
184+
)
146185

147-
msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema(
148-
attribute_reports=attrs
186+
hdr = zcl_f.ZCLHeader(
187+
frame_control=frame_control,
188+
manufacturer=manufacturer,
189+
tsn=tsn,
190+
command_id=zcl_f.GeneralCommand.Report_Attributes,
149191
)
150192

151-
hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes)
152-
hdr.frame_control = hdr.frame_control.replace(disable_default_response=True)
153-
cluster.handle_message(hdr, msg)
193+
command = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema(
194+
attribute_reports=reports
195+
)
196+
197+
cluster.handle_cluster_general_request(hdr, command)
198+
154199
await zha_gateway.async_block_till_done()
155200

156201

tests/test_binary_sensor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import zigpy.profiles.zha
1010
from zigpy.quirks import DeviceRegistry
1111
from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder
12+
from zigpy.typing import UNDEFINED
1213
from zigpy.zcl.clusters import general, measurement, security
1314
from zigpy.zcl.clusters.general import OnOff
1415

@@ -107,7 +108,7 @@ async def async_test_binary_sensor_occupancy(
107108
await zha_gateway.async_block_till_done()
108109
assert cluster.read_attributes.await_count == 1
109110
assert cluster.read_attributes.await_args == call(
110-
["occupancy"], allow_cache=True, only_cache=True, manufacturer=None
111+
["occupancy"], allow_cache=True, only_cache=True, manufacturer=UNDEFINED
111112
)
112113
assert entity.is_on
113114

@@ -143,7 +144,7 @@ async def async_test_iaszone_on_off(
143144
await zha_gateway.async_block_till_done()
144145
assert cluster.read_attributes.await_count == 1
145146
assert cluster.read_attributes.await_args == call(
146-
["zone_status"], allow_cache=False, only_cache=False, manufacturer=None
147+
["zone_status"], allow_cache=False, only_cache=False, manufacturer=UNDEFINED
147148
)
148149
assert entity.is_on
149150

@@ -234,7 +235,7 @@ async def test_smarttthings_multi(
234235

235236
st_ch.emit_zha_event = MagicMock(wraps=st_ch.emit_zha_event)
236237

237-
await send_attributes_report(zha_gateway, st_ch.cluster, {0x0012: 120})
238+
await send_attributes_report(zha_gateway, st_ch.cluster, {"x_axis": 120})
238239

239240
assert st_ch.emit_zha_event.call_count == 1
240241
assert st_ch.emit_zha_event.mock_calls == [

tests/test_button.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from zigpy.quirks import CustomCluster, CustomDevice, DeviceRegistry
1919
from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder
2020
import zigpy.types as t
21+
from zigpy.typing import UNDEFINED
2122
from zigpy.zcl.clusters import general, security
2223
from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
2324
import zigpy.zcl.foundation as zcl_f
@@ -155,7 +156,7 @@ async def test_frost_unlock(
155156
await entity.async_press()
156157
await zha_gateway.async_block_till_done()
157158
assert cluster.write_attributes.mock_calls == [
158-
call({"frost_lock_reset": 0}, manufacturer=None)
159+
call({"frost_lock_reset": 0}, manufacturer=UNDEFINED)
159160
]
160161

161162
cluster.write_attributes.reset_mock()
@@ -167,9 +168,9 @@ async def test_frost_unlock(
167168

168169
# There are three retries
169170
assert cluster.write_attributes.mock_calls == [
170-
call({"frost_lock_reset": 0}, manufacturer=None),
171-
call({"frost_lock_reset": 0}, manufacturer=None),
172-
call({"frost_lock_reset": 0}, manufacturer=None),
171+
call({"frost_lock_reset": 0}, manufacturer=UNDEFINED),
172+
call({"frost_lock_reset": 0}, manufacturer=UNDEFINED),
173+
call({"frost_lock_reset": 0}, manufacturer=UNDEFINED),
173174
]
174175

175176

@@ -290,7 +291,7 @@ async def test_quirks_write_attr_button(
290291
await entity.async_press()
291292
await zha_gateway.async_block_till_done()
292293
assert cluster.write_attributes.mock_calls == [
293-
call({cluster.AttributeDefs.feed.name: 2}, manufacturer=None)
294+
call({cluster.AttributeDefs.feed.name: 2}, manufacturer=UNDEFINED)
294295
]
295296

296297
assert cluster.get(cluster.AttributeDefs.feed.name) == 2

tests/test_device.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from zigpy.quirks.v2.homeassistant import EntityType
2020
from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
2121
import zigpy.types
22+
from zigpy.typing import UNDEFINED
2223
from zigpy.zcl import ClusterType
2324
from zigpy.zcl.clusters import general
2425
from zigpy.zcl.clusters.general import Ota, PowerConfiguration
@@ -554,7 +555,7 @@ async def test_write_zigbee_attribute(
554555
{
555556
general.OnOff.AttributeDefs.start_up_on_off.id: general.OnOff.StartUpOnOff.PreviousValue
556557
},
557-
manufacturer=None,
558+
manufacturer=UNDEFINED,
558559
)
559560

560561
cluster.write_attributes.reset_mock()

tests/test_fan.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from zigpy.device import Device as ZigpyDevice
1212
from zigpy.exceptions import ZigbeeException
1313
from zigpy.profiles import zha
14+
from zigpy.typing import UNDEFINED
1415
from zigpy.zcl.clusters import general, hvac
1516
import zigpy.zcl.foundation as zcl_f
1617
import zigpy.zdo.types as zdo_t
@@ -159,7 +160,7 @@ async def test_fan(
159160
await async_turn_on(zha_gateway, entity)
160161
assert len(cluster.write_attributes.mock_calls) == 1
161162
assert cluster.write_attributes.call_args == call(
162-
{"fan_mode": 2}, manufacturer=None
163+
{"fan_mode": 2}, manufacturer=UNDEFINED
163164
)
164165
assert entity.state["is_on"] is True
165166

@@ -168,7 +169,7 @@ async def test_fan(
168169
await async_turn_off(zha_gateway, entity)
169170
assert len(cluster.write_attributes.mock_calls) == 1
170171
assert cluster.write_attributes.call_args == call(
171-
{"fan_mode": 0}, manufacturer=None
172+
{"fan_mode": 0}, manufacturer=UNDEFINED
172173
)
173174
assert entity.state["is_on"] is False
174175

@@ -177,7 +178,7 @@ async def test_fan(
177178
await async_set_speed(zha_gateway, entity, speed=SPEED_HIGH)
178179
assert len(cluster.write_attributes.mock_calls) == 1
179180
assert cluster.write_attributes.call_args == call(
180-
{"fan_mode": 3}, manufacturer=None
181+
{"fan_mode": 3}, manufacturer=UNDEFINED
181182
)
182183
assert entity.state["is_on"] is True
183184
assert entity.state["speed"] == SPEED_HIGH
@@ -187,7 +188,7 @@ async def test_fan(
187188
await async_set_preset_mode(zha_gateway, entity, preset_mode=PRESET_MODE_ON)
188189
assert len(cluster.write_attributes.mock_calls) == 1
189190
assert cluster.write_attributes.call_args == call(
190-
{"fan_mode": 4}, manufacturer=None
191+
{"fan_mode": 4}, manufacturer=UNDEFINED
191192
)
192193
assert entity.state["is_on"] is True
193194
assert entity.state["preset_mode"] == PRESET_MODE_ON
@@ -198,7 +199,7 @@ async def test_fan(
198199
await zha_gateway.async_block_till_done()
199200
assert len(cluster.write_attributes.mock_calls) == 1
200201
assert cluster.write_attributes.call_args == call(
201-
{"fan_mode": 2}, manufacturer=None
202+
{"fan_mode": 2}, manufacturer=UNDEFINED
202203
)
203204
# this is converted to a ranged value
204205
assert entity.state["percentage"] == 66
@@ -566,42 +567,42 @@ async def test_fan_ikea(
566567
cluster.write_attributes.reset_mock()
567568
await async_turn_on(zha_gateway, entity)
568569
assert cluster.write_attributes.mock_calls == [
569-
call({"fan_mode": 1}, manufacturer=None)
570+
call({"fan_mode": 1}, manufacturer=UNDEFINED)
570571
]
571572

572573
# turn on with set speed from HA
573574
cluster.write_attributes.reset_mock()
574575
await async_turn_on(zha_gateway, entity, speed="high")
575576
assert cluster.write_attributes.mock_calls == [
576-
call({"fan_mode": 10}, manufacturer=None)
577+
call({"fan_mode": 10}, manufacturer=UNDEFINED)
577578
]
578579

579580
# turn off from HA
580581
cluster.write_attributes.reset_mock()
581582
await async_turn_off(zha_gateway, entity)
582583
assert cluster.write_attributes.mock_calls == [
583-
call({"fan_mode": 0}, manufacturer=None)
584+
call({"fan_mode": 0}, manufacturer=UNDEFINED)
584585
]
585586

586587
# change speed from HA
587588
cluster.write_attributes.reset_mock()
588589
await async_set_percentage(zha_gateway, entity, percentage=100)
589590
assert cluster.write_attributes.mock_calls == [
590-
call({"fan_mode": 10}, manufacturer=None)
591+
call({"fan_mode": 10}, manufacturer=UNDEFINED)
591592
]
592593

593594
# skip 10% when set from HA
594595
cluster.write_attributes.reset_mock()
595596
await async_set_percentage(zha_gateway, entity, percentage=10)
596597
assert cluster.write_attributes.mock_calls == [
597-
call({"fan_mode": 2}, manufacturer=None)
598+
call({"fan_mode": 2}, manufacturer=UNDEFINED)
598599
]
599600

600601
# change preset_mode from HA
601602
cluster.write_attributes.reset_mock()
602603
await async_set_preset_mode(zha_gateway, entity, preset_mode=PRESET_MODE_AUTO)
603604
assert cluster.write_attributes.mock_calls == [
604-
call({"fan_mode": 1}, manufacturer=None)
605+
call({"fan_mode": 1}, manufacturer=UNDEFINED)
605606
]
606607

607608
# set invalid preset_mode from HA
@@ -750,28 +751,28 @@ async def test_fan_kof(
750751
cluster.write_attributes.reset_mock()
751752
await async_turn_on(zha_gateway, entity)
752753
assert cluster.write_attributes.mock_calls == [
753-
call({"fan_mode": 2}, manufacturer=None)
754+
call({"fan_mode": 2}, manufacturer=UNDEFINED)
754755
]
755756

756757
# turn off from HA
757758
cluster.write_attributes.reset_mock()
758759
await async_turn_off(zha_gateway, entity)
759760
assert cluster.write_attributes.mock_calls == [
760-
call({"fan_mode": 0}, manufacturer=None)
761+
call({"fan_mode": 0}, manufacturer=UNDEFINED)
761762
]
762763

763764
# change speed from HA
764765
cluster.write_attributes.reset_mock()
765766
await async_set_percentage(zha_gateway, entity, percentage=100)
766767
assert cluster.write_attributes.mock_calls == [
767-
call({"fan_mode": 4}, manufacturer=None)
768+
call({"fan_mode": 4}, manufacturer=UNDEFINED)
768769
]
769770

770771
# change preset_mode from HA
771772
cluster.write_attributes.reset_mock()
772773
await async_set_preset_mode(zha_gateway, entity, preset_mode=PRESET_MODE_SMART)
773774
assert cluster.write_attributes.mock_calls == [
774-
call({"fan_mode": 6}, manufacturer=None)
775+
call({"fan_mode": 6}, manufacturer=UNDEFINED)
775776
]
776777

777778
# set invalid preset_mode from HA

0 commit comments

Comments
 (0)