Skip to content

Commit d26dce5

Browse files
committed
Merge branch 'release/0.0.69'
2 parents dcc8516 + afd84ae commit d26dce5

File tree

5 files changed

+71
-29
lines changed

5 files changed

+71
-29
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ build/**/*.*
1010
.coverage
1111
htmlcov/
1212
.mypycache
13+
# Environments
14+
.env
15+
.venv
16+
env/
17+
venv/
18+
ENV/
19+
env.bak/
20+
venv.bak/

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from setuptools import find_packages, setup
66

7-
VERSION = "0.0.68"
7+
VERSION = "0.0.69"
88

99

1010
setup(

tests/test_quirks.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
"""General quirk tests."""
2+
from __future__ import annotations
23

4+
from pathlib import Path
35
from unittest import mock
46

57
import pytest
68
import zigpy.device
79
import zigpy.endpoint
810
import zigpy.profiles
911
import zigpy.quirks as zq
12+
from zigpy.quirks import CustomDevice
1013
import zigpy.types
1114
import zigpy.zcl as zcl
1215
import zigpy.zdo.types
1316

1417
import zhaquirks
18+
import zhaquirks.bosch.motion
1519
from zhaquirks.const import (
1620
DEVICE_TYPE,
1721
ENDPOINTS,
@@ -62,7 +66,7 @@
6266

6367

6468
@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES)
65-
def test_quirk_replacements(quirk):
69+
def test_quirk_replacements(quirk: CustomDevice) -> None:
6670
"""Test all quirks have a replacement."""
6771

6872
assert quirk.signature
@@ -72,15 +76,15 @@ def test_quirk_replacements(quirk):
7276

7377

7478
@pytest.fixture
75-
def raw_device():
79+
def raw_device() -> zigpy.device.Device:
7680
"""Raw device."""
7781
app = mock.MagicMock()
7882
ieee = zigpy.types.EUI64.convert("11:22:33:44:55:66:77:88")
7983
nwk = 0x1234
8084
return zigpy.device.Device(app, ieee, nwk)
8185

8286

83-
def test_dev_from_signature_incomplete_sig(raw_device):
87+
def test_dev_from_signature_incomplete_sig(raw_device: zigpy.device.Device) -> None:
8488
"""Test device initialization from quirk's based on incomplete signature."""
8589

8690
class BadSigNoSignature(zhaquirks.QuickInitDevice):
@@ -224,7 +228,9 @@ class BadSigIncompleteEp(zhaquirks.QuickInitDevice):
224228
},
225229
),
226230
)
227-
def test_dev_from_signature(raw_device, quirk_signature):
231+
def test_dev_from_signature(
232+
raw_device: zigpy.device.Device, quirk_signature: dict
233+
) -> None:
228234
"""Test device initialization from quirk's based on signature."""
229235

230236
class QuirkDevice(zhaquirks.QuickInitDevice):
@@ -255,7 +261,7 @@ class QuirkDevice(zhaquirks.QuickInitDevice):
255261
@pytest.mark.parametrize(
256262
"quirk", (q for q in ALL_QUIRK_CLASSES if issubclass(q, zhaquirks.QuickInitDevice))
257263
)
258-
def test_quirk_quickinit(quirk):
264+
def test_quirk_quickinit(quirk: zigpy.quirks.CustomDevice) -> None:
259265
"""Make sure signature in QuickInit Devices have all required attributes."""
260266

261267
if not issubclass(quirk, zhaquirks.QuickInitDevice):
@@ -273,10 +279,10 @@ def test_quirk_quickinit(quirk):
273279

274280

275281
@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES)
276-
def test_signature(quirk):
282+
def test_signature(quirk: CustomDevice) -> None:
277283
"""Make sure signature look sane for all custom devices."""
278284

279-
def _check_range(cluster):
285+
def _check_range(cluster: zcl.Cluster) -> bool:
280286
for range in zcl.Cluster._registry_range.keys():
281287
if range[0] <= cluster <= range[1]:
282288
return True
@@ -360,7 +366,7 @@ def _check_range(cluster):
360366

361367

362368
@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES)
363-
def test_quirk_importable(quirk):
369+
def test_quirk_importable(quirk: CustomDevice) -> None:
364370
"""Ensure all quirks can be imported with a normal Python `import` statement."""
365371

366372
path = f"{quirk.__module__}.{quirk.__name__}"
@@ -369,7 +375,7 @@ def test_quirk_importable(quirk):
369375
), f"{path} is not importable"
370376

371377

372-
def test_quirk_loading_error(tmp_path):
378+
def test_quirk_loading_error(tmp_path: Path) -> None:
373379
"""Ensure quirks do not silently fail to load."""
374380

375381
custom_quirks = tmp_path / "custom_zha_quirks"
@@ -394,7 +400,9 @@ def test_quirk_loading_error(tmp_path):
394400
zhaquirks.setup({zhaquirks.CUSTOM_QUIRKS_PATH: str(custom_quirks)})
395401

396402

397-
def test_custom_quirk_loading(zigpy_device_from_quirk, tmp_path):
403+
def test_custom_quirk_loading(
404+
zigpy_device_from_quirk: CustomDevice, tmp_path: Path
405+
) -> None:
398406
"""Make sure custom quirks take priority over regular quirks."""
399407

400408
device = zigpy_device_from_quirk(
@@ -486,3 +494,17 @@ class TestReplacementISWZPR1WP13(CustomDevice):
486494

487495
assert not isinstance(zq.get_device(device), zhaquirks.bosch.motion.ISWZPR1WP13)
488496
assert type(zq.get_device(device)).__name__ == "TestReplacementISWZPR1WP13"
497+
498+
499+
def test_zigpy_custom_cluster_pollution() -> None:
500+
"""Ensure all quirks subclass `CustomCluster`."""
501+
non_zigpy_clusters = {
502+
cluster
503+
for cluster in zcl.Cluster._registry.values()
504+
if not cluster.__module__.startswith("zigpy.")
505+
}
506+
507+
if non_zigpy_clusters:
508+
raise RuntimeError(
509+
f"Custom clusters must subclass `CustomCluster`: {non_zigpy_clusters}"
510+
)

zhaquirks/tuya/ts0211.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from zigpy.zcl.clusters.homeautomation import Diagnostic
1111
from zigpy.zcl.clusters.security import IasZone
1212

13+
from zhaquirks import CustomCluster
1314
from zhaquirks.const import (
1415
COMMAND_SINGLE,
1516
DEVICE_TYPE,
@@ -22,7 +23,7 @@
2223
)
2324

2425

25-
class IasZoneDoorbellCluster(IasZone):
26+
class IasZoneDoorbellCluster(CustomCluster, IasZone):
2627
"""Custom IasZone cluster for the doorbell."""
2728

2829
cluster_id = IasZone.cluster_id

zhaquirks/xiaomi/aqara/roller_curtain_e1.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Aqara Roller Shade Driver E1 device."""
2+
from __future__ import annotations
3+
4+
from typing import Any
25

36
from zigpy import types as t
47
from zigpy.profiles import zha
8+
from zigpy.zcl import foundation
59
from zigpy.zcl.clusters.closures import WindowCovering
610
from zigpy.zcl.clusters.general import (
711
Alarms,
@@ -20,7 +24,7 @@
2024
)
2125
from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
2226

23-
from zhaquirks import Bus, LocalDataCluster
27+
from zhaquirks import Bus, CustomCluster, LocalDataCluster
2428
from zhaquirks.const import (
2529
DEVICE_TYPE,
2630
ENDPOINTS,
@@ -57,12 +61,12 @@ class XiaomiAqaraRollerE1(XiaomiCluster, ManufacturerSpecificCluster):
5761
)
5862

5963

60-
class AnalogOutputRollerE1(AnalogOutput):
64+
class AnalogOutputRollerE1(CustomCluster, AnalogOutput):
6165
"""Analog output cluster, only used to relay current_value to WindowCovering."""
6266

6367
cluster_id = AnalogOutput.cluster_id
6468

65-
def __init__(self, *args, **kwargs):
69+
def __init__(self, *args: Any, **kwargs: Any) -> None:
6670
"""Init."""
6771
super().__init__(*args, **kwargs)
6872

@@ -72,7 +76,7 @@ def __init__(self, *args, **kwargs):
7276
self._update_attribute(0x006A, 1.0) # resolution
7377
self._update_attribute(0x006F, 0x00) # status_flags
7478

75-
def _update_attribute(self, attrid, value):
79+
def _update_attribute(self, attrid: int, value: Any) -> None:
7680

7781
super()._update_attribute(attrid, value)
7882

@@ -82,18 +86,25 @@ def _update_attribute(self, attrid, value):
8286
)
8387

8488

85-
class WindowCoveringRollerE1(WindowCovering):
89+
class WindowCoveringRollerE1(CustomCluster, WindowCovering):
8690
"""Window covering cluster to receive commands that are sent to the AnalogOutput's present_value to move the motor."""
8791

8892
cluster_id = WindowCovering.cluster_id
8993

90-
def __init__(self, *args, **kwargs):
94+
def __init__(self, *args: Any, **kwargs: Any) -> None:
9195
"""Init."""
9296
super().__init__(*args, **kwargs)
9397

9498
async def command(
95-
self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None
96-
):
99+
self,
100+
command_id: foundation.GeneralCommand | int | t.uint8_t,
101+
*args: Any,
102+
manufacturer: int | t.uint16_t | None = None,
103+
expect_reply: bool = True,
104+
tries: int = 1,
105+
tsn: int | t.uint8_t | None = None,
106+
**kwargs: Any,
107+
) -> Any:
97108
"""Overwrite the commands to make it work for both firmware 1425 and 1427.
98109
99110
We either overwrite analog_output's current_value or multistate_output's current
@@ -104,24 +115,24 @@ async def command(
104115
{"present_value": 1}
105116
)
106117
return res[0].status
107-
elif command_id == DOWN_CLOSE:
118+
if command_id == DOWN_CLOSE:
108119
(res,) = await self.endpoint.multistate_output.write_attributes(
109120
{"present_value": 0}
110121
)
111122
return res[0].status
112-
elif command_id == GO_TO_LIFT_PERCENTAGE:
123+
if command_id == GO_TO_LIFT_PERCENTAGE:
113124
(res,) = await self.endpoint.analog_output.write_attributes(
114125
{"present_value": (100 - args[0])}
115126
)
116127
return res[0].status
117-
elif command_id == STOP:
128+
if command_id == STOP:
118129
(res,) = await self.endpoint.multistate_output.write_attributes(
119130
{"present_value": 2}
120131
)
121132
return res[0].status
122133

123134

124-
class MultistateOutputRollerE1(MultistateOutput):
135+
class MultistateOutputRollerE1(CustomCluster, MultistateOutput):
125136
"""Multistate Output cluster which overwrites present_value.
126137
127138
Otherwise, it gives errors of wrong datatype when using it in the commands.
@@ -140,12 +151,12 @@ class PowerConfigurationRollerE1(PowerConfiguration, LocalDataCluster):
140151

141152
BATTERY_PERCENTAGE_REMAINING = 0x0021
142153

143-
def __init__(self, *args, **kwargs):
154+
def __init__(self, *args: Any, **kwargs: Any) -> None:
144155
"""Init."""
145156
super().__init__(*args, **kwargs)
146157
self.endpoint.device.power_bus_percentage.add_listener(self)
147158

148-
def update_battery_percentage(self, value):
159+
def update_battery_percentage(self, value: int) -> None:
149160
"""Doubles the battery percentage to the Zigbee spec's expected 200% maximum."""
150161
super()._update_attribute(
151162
self.BATTERY_PERCENTAGE_REMAINING,
@@ -156,10 +167,10 @@ def update_battery_percentage(self, value):
156167
class RollerE1AQ(XiaomiCustomDevice):
157168
"""Aqara Roller Shade Driver E1 device."""
158169

159-
def __init__(self, *args, **kwargs):
170+
def __init__(self, *args: Any, **kwargs: Any) -> None:
160171
"""Init."""
161-
self.power_bus_percentage = Bus()
162-
super().__init__(*args, **kwargs)
172+
self.power_bus_percentage: Bus = Bus() # type: ignore
173+
super().__init__(*args, **kwargs) # type: ignore
163174

164175
signature = {
165176
MODELS_INFO: [(LUMI, "lumi.curtain.acn002")],

0 commit comments

Comments
 (0)