Skip to content

Commit dcc8516

Browse files
committed
Merge branch 'release/0.0.68'
2 parents 83057c4 + 8e8f34b commit dcc8516

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+2808
-798
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ build/**/*.*
88
.tox
99
.idea/*
1010
.coverage
11+
htmlcov/
1112
.mypycache

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ profile = black
77
not_skip = __init__.py
88
# will group `import x` and `from x import` of the same module.
99
force_sort_within_sections = true
10-
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
10+
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
1111
default_section = THIRDPARTY
1212
known_first_party = zhaquirks,tests
1313
forced_separate = tests

.pre-commit-config.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/asottile/pyupgrade
3-
rev: v2.31.0
3+
rev: v2.31.1
44
hooks:
55
- id: pyupgrade
66

@@ -10,23 +10,23 @@ repos:
1010
- id: autoflake8
1111

1212
- repo: https://github.com/psf/black
13-
rev: 20.8b1
13+
rev: 22.3.0
1414
hooks:
1515
- id: black
1616
args:
1717
- --safe
1818
- --quiet
1919

2020
- repo: https://gitlab.com/pycqa/flake8
21-
rev: 3.8.4
21+
rev: 4.0.1
2222
hooks:
2323
- id: flake8
2424
additional_dependencies:
25-
- flake8-docstrings==1.5.0
26-
- pydocstyle==5.1.1
25+
- flake8-docstrings==1.6.0
26+
- pydocstyle==6.1.1
2727

2828
- repo: https://github.com/PyCQA/isort
29-
rev: 5.5.2
29+
rev: 5.10.1
3030
hooks:
3131
- id: isort
3232

@@ -36,6 +36,6 @@ repos:
3636
- id: codespell
3737

3838
- repo: https://github.com/pre-commit/mirrors-mypy
39-
rev: v0.902
39+
rev: v0.942
4040
hooks:
4141
- id: mypy

README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,119 @@ If you look at another example for the same device:
422422

423423
You can see a pattern that illustrates how to match a more complex event. In this case the step command is used for the dim up and dim down buttons so we need to match more of the event data to uniquely match the event.
424424

425+
## Testing using unit tests
426+
427+
The tests use the [pytest](https://docs.pytest.org/en/latest/) framework.
428+
429+
### Getting started
430+
431+
To get set up, you need install the test dependencies:
432+
433+
```bash
434+
pip install -r requirements_test_all.txt
435+
```
436+
437+
### Running the tests
438+
439+
See the [pytest documentation](https://docs.pytest.org/en/latest/) for details about how to run
440+
the tests. For example, to run all the `test_tuya.py` tests:
441+
442+
```bash
443+
$ pytest --disable-warnings tests/test_tuya.py
444+
Test session starts (platform: linux, Python 3.9.2, pytest 6.2.5, pytest-sugar 0.9.4)
445+
446+
collecting ...
447+
tests/test_tuya.py ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 100% ██████████
448+
449+
Results (3.58s):
450+
41 passed
451+
```
452+
453+
### Writing tests
454+
455+
To add a new test, start by adding a new function to one of the existing test files. You
456+
can follow the instructions in the [Getting started](https://docs.pytest.org/en/latest/getting-started.html)
457+
section of the pytest documentation.
458+
459+
### Using fixtures to set things up
460+
461+
In order to write a test, you will need to access an instance of a quirk to run the tests against. Pytest
462+
provides a useful feature called Fixtures that allow you to write and use the setup code necessary in one
463+
place, similar to how we use libraries to provide common functions to other code.
464+
465+
You can read more about fixtures [here](https://docs.pytest.org/en/latest/how-to/fixtures.html#how-to-fixtures).
466+
467+
You can find the common fixtures in files named `conftest.py`. Pytest will list them for you as follows:
468+
469+
```bash
470+
$ pytest --fxitures
471+
[...]
472+
--- fixtures defined from tests.conftest ---
473+
MockAppController
474+
App controller mock.
475+
476+
ieee_mock
477+
Return a static ieee.
478+
479+
zigpy_device_mock
480+
Zigpy device mock.
481+
482+
zigpy_device_from_quirk
483+
Create zigpy device from Quirks signature.
484+
485+
[...]
486+
--- fixtures defined from tests.test_tuya_clusters ---
487+
TuyaCluster
488+
Mock of the new Tuya manufacturer cluster.
489+
```
490+
491+
Some fixtures such as `app_controller_mock` will provide an object instance that you can
492+
use directly. Others, such as `zigpy_device_mock` will return a function, which you can
493+
call to create a customised object during your own setup.
494+
495+
### Testing the quirk signature matching
496+
497+
The fixture `assert_signature_matches_quirk` provides a function that can be
498+
used to check that a particular device signature matches the corresponding quirk.
499+
By capturing the signature and adding a few lines to the test file, this means that
500+
you can verify that your device will be matched against the quirk without needing to
501+
go through the paring process directly.
502+
503+
You need to capture the device signature and save it. If you have previously started the
504+
pairing process in Home assistant, you can find the signature under 'Zigbee Device Signature'
505+
on the device page.
506+
507+
Now you can create a test that checks the signature as follows:
508+
509+
```python
510+
def test_ts0121_signature(assert_signature_matches_quirk):
511+
signature = {
512+
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
513+
"endpoints": {
514+
"1": {
515+
"profile_id": 260,
516+
"device_type": "0x0051",
517+
"in_clusters": [
518+
"0x0000",
519+
"0x0004",
520+
"0x0005",
521+
"0x0006",
522+
"0x0702",
523+
"0x0b04"
524+
],
525+
"out_clusters": [
526+
"0x000a",
527+
"0x0019"
528+
]
529+
}
530+
},
531+
"manufacturer": "_TZ3000_g5xawfcq",
532+
"model": "TS0121",
533+
"class": "zhaquirks.tuya.ts0121_plug.Plug"
534+
}
535+
assert_signature_matches_quirk(zhaquirks.tuya.ts0121_plug.Plug, signature)
536+
```
537+
425538
# Testing new releases
426539

427540
Testing a new release of the zha-quirks package before it is released in Home Assistant.

requirements_test_all.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ codecov==2.1.10
77
coveralls==2.2.0
88
mock-open==1.4.0
99
mypy==0.790
10-
pre-commit==2.8.2
10+
pre-commit==2.9.2
1111
pylint==2.6.0
1212
pytest-aiohttp==0.3.0
1313
pytest-cov==2.10.1

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ profile = black
3131
not_skip = __init__.py
3232
# will group `import x` and `from x import` of the same module.
3333
force_sort_within_sections = true
34-
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
34+
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
3535
default_section = THIRDPARTY
3636
known_first_party = zhaquirks,tests
3737
forced_separate = tests

setup.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
"""Setup module for ZHAQuirks."""
22

3-
from setuptools import find_packages, setup
4-
5-
VERSION = "0.0.67"
3+
import pathlib
64

5+
from setuptools import find_packages, setup
76

8-
def readme():
9-
"""Print long description."""
10-
with open("README.md") as f:
11-
return f.read()
7+
VERSION = "0.0.68"
128

139

1410
setup(
1511
name="zha-quirks",
1612
version=VERSION,
1713
description="Library implementing Zigpy quirks for ZHA in Home Assistant",
18-
long_description=readme(),
14+
long_description=(pathlib.Path(__file__).parent / "README.md").read_text(),
1915
long_description_content_type="text/markdown",
2016
url="https://github.com/dmulcahey/zha-device-handlers",
2117
author="David F. Mulcahey",
@@ -24,6 +20,6 @@ def readme():
2420
keywords="zha quirks homeassistant hass",
2521
packages=find_packages(exclude=["tests"]),
2622
python_requires=">=3",
27-
install_requires=["zigpy>=0.42.0"],
23+
install_requires=["zigpy>=0.44.1"],
2824
tests_require=["pytest"],
2925
)

tests/conftest.py

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
"""Fixtures for all tests."""
22

3-
from asynctest import CoroutineMock
3+
try:
4+
from unittest.mock import AsyncMock as CoroutineMock
5+
except ImportError:
6+
from asynctest import CoroutineMock
7+
48
import pytest
59
import zigpy.application
610
import zigpy.device
11+
import zigpy.quirks
712
import zigpy.types
13+
import zigpy.zcl.foundation as foundation
814

915
from zhaquirks.const import (
1016
DEVICE_TYPE,
@@ -31,17 +37,41 @@ async def probe(self, *args):
3137
"""Probe method."""
3238
return True
3339

34-
async def shutdown(self):
35-
"""Mock shutdown."""
36-
3740
async def startup(self, *args):
3841
"""Mock startup."""
3942

43+
async def shutdown(self, *args):
44+
"""Mock shutdown."""
45+
4046
async def permit_ncp(self, *args):
4147
"""Mock permit ncp."""
4248

49+
async def broadcast(self, *args, **kwargs):
50+
"""Mock broadcast."""
51+
52+
async def connect(self, *args, **kwargs):
53+
"""Mock connect."""
54+
55+
async def disconnect(self, *args, **kwargs):
56+
"""Mock disconnect."""
57+
58+
async def force_remove(self, *args, **kwargs):
59+
"""Mock force_remove."""
60+
61+
async def load_network_info(self, *args, **kwargs):
62+
"""Mock load_network_info."""
63+
64+
async def permit_with_key(self, *args, **kwargs):
65+
"""Mock permit_with_key."""
66+
67+
async def start_network(self, *args, **kwargs):
68+
"""Mock start_network."""
69+
70+
async def write_network_info(self, *args, **kwargs):
71+
"""Mock write_network_info."""
72+
4373
mrequest = CoroutineMock()
44-
request = CoroutineMock()
74+
request = CoroutineMock(return_value=(foundation.Status.SUCCESS, None))
4575

4676

4777
@pytest.fixture(name="MockAppController")
@@ -115,3 +145,47 @@ def _dev(quirk, ieee=None, nwk=zigpy.types.NWK(0x1234), apply_quirk=True):
115145
return device
116146

117147
return _dev
148+
149+
150+
@pytest.fixture
151+
def assert_signature_matches_quirk():
152+
"""Return a function that can be used to check if a given quirk matches a signature."""
153+
154+
def _check(quirk, signature):
155+
# Check device signature as copied from Zigbee device signature window for the device
156+
class FakeDevEndpoint:
157+
def __init__(self, endpoint):
158+
self.endpoint = endpoint
159+
160+
def __getattr__(self, key):
161+
if key == "device_type":
162+
return int(self.endpoint[key], 16)
163+
elif key in ("in_clusters", "out_clusters"):
164+
return [int(cluster_id, 16) for cluster_id in self.endpoint[key]]
165+
else:
166+
return self.endpoint[key]
167+
168+
class FakeDevice:
169+
nwk = 0
170+
171+
def __init__(self, signature):
172+
self.endpoints = {
173+
int(id): FakeDevEndpoint(ep)
174+
for id, ep in signature["endpoints"].items()
175+
}
176+
for attr in ("manufacturer", "model", "ieee"):
177+
setattr(self, attr, signature.get(attr))
178+
179+
def __getitem__(self, key):
180+
# Return item from signature, or None if not given
181+
return self.endpoints.get(key)
182+
183+
def __getattr__(self, key):
184+
# Return item from signature, or None if not given
185+
return self.endpoints.get(key)
186+
187+
test_dev = FakeDevice(signature)
188+
device = zigpy.quirks.get_device(test_dev)
189+
assert isinstance(device, quirk)
190+
191+
return _check

tests/test_quirks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,8 @@ def _check_range(cluster):
317317
for clusters_type in (INPUT_CLUSTERS, OUTPUT_CLUSTERS):
318318
clusters = ep_data.get(clusters_type)
319319
if clusters is not None:
320-
assert all((isinstance(cluster_id, int) for cluster_id in clusters))
321-
assert all((0 <= cluster_id <= 0xFFFF for cluster_id in clusters))
320+
assert all(isinstance(cluster_id, int) for cluster_id in clusters)
321+
assert all(0 <= cluster_id <= 0xFFFF for cluster_id in clusters)
322322

323323
for m_m in (MANUFACTURER, MODEL):
324324
value = ep_data.get(m_m)

0 commit comments

Comments
 (0)