Skip to content

Commit f4e68e9

Browse files
authored
0.9.1 Release
2 parents 06c0698 + d7532b6 commit f4e68e9

File tree

7 files changed

+131
-41
lines changed

7 files changed

+131
-41
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@ The goal of this project to add native support for the Dresden-Elektronik deCONZ
99

1010
This library uses the deCONZ serial protocol for communicating with [ConBee](https://www.dresden-elektronik.de/conbee/), [ConBee II (ConBee 2)](https://shop.dresden-elektronik.de/conbee-2.html), and [RaspBee](https://www.dresden-elektronik.de/raspbee/) adapters from [Dresden-Elektronik](https://github.com/dresden-elektronik/).
1111

12+
# Testing new releases
13+
14+
Testing a new release of the zigpy-deconz library before it is released in Home Assistant.
15+
16+
If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro):
17+
- Add https://github.com/home-assistant/hassio-addons-development as "add-on" repository
18+
- Install "Custom deps deployment" addon
19+
- Update config like:
20+
```
21+
pypi:
22+
- zigpy-deconz==0.9.0
23+
apk: []
24+
```
25+
where 0.5.1 is the new version
26+
- Start the addon
27+
28+
If you are instead using some custom python installation of Home Assistant then do this:
29+
- Activate your python virtual env
30+
- Update package with ``pip``
31+
```
32+
pip install zigpy-deconz==0.9.0
33+
1234
# Releases via PyPI
1335
Tagged versions are also released via PyPI
1436

tests/test_api.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
@pytest.fixture
17-
def api():
17+
def api(event_loop):
1818
controller = mock.MagicMock(
1919
spec_set=zigpy_deconz.zigbee.application.ControllerApplication
2020
)
@@ -82,6 +82,26 @@ async def mock_fut():
8282
api._uart.send.reset_mock()
8383

8484

85+
@pytest.mark.asyncio
86+
async def test_command_queue(api, monkeypatch):
87+
def mock_api_frame(name, *args):
88+
return mock.sentinel.api_frame_data, api._seq
89+
90+
api._api_frame = mock.MagicMock(side_effect=mock_api_frame)
91+
api._uart.send = mock.MagicMock()
92+
93+
monkeypatch.setattr(deconz_api, "COMMAND_TIMEOUT", 0.1)
94+
95+
for cmd, cmd_opts in deconz_api.TX_COMMANDS.items():
96+
async with api._command_lock:
97+
with pytest.raises(asyncio.TimeoutError):
98+
await asyncio.wait_for(api._command(cmd, mock.sentinel.cmd_data), 0.1)
99+
assert api._api_frame.call_count == 0
100+
assert api._uart.send.call_count == 0
101+
api._api_frame.reset_mock()
102+
api._uart.send.reset_mock()
103+
104+
85105
@pytest.mark.asyncio
86106
async def test_command_timeout(api, monkeypatch):
87107
def mock_api_frame(name, *args):

tests/test_application.py

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515

1616

1717
@pytest.fixture
18-
def app(database_file=None):
18+
def device_path():
19+
return "/dev/null"
20+
21+
22+
@pytest.fixture
23+
def app(device_path, database_file=None):
1924
config = application.ControllerApplication.SCHEMA(
2025
{
21-
zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/null"},
26+
zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: device_path},
2227
zigpy.config.CONF_DATABASE: database_file,
2328
}
2429
)
@@ -215,7 +220,7 @@ async def _version():
215220
api.version = mock.MagicMock(side_effect=_version)
216221
api.write_parameter = CoroutineMock()
217222

218-
monkeypatch.setattr(application.ConBeeDevice, "new", CoroutineMock())
223+
monkeypatch.setattr(application.DeconzDevice, "new", CoroutineMock())
219224
with mock.patch.object(application, "Deconz", return_value=api):
220225
await app.startup(auto_form=False)
221226
assert app.form_network.call_count == 0
@@ -393,19 +398,19 @@ def test_rx_device_annce(app, addr_ieee, addr_nwk):
393398

394399

395400
@pytest.mark.asyncio
396-
async def test_conbee_dev_add_to_group(app, nwk):
401+
async def test_deconz_dev_add_to_group(app, nwk, device_path):
397402
group = mock.MagicMock()
398403
app._groups = mock.MagicMock()
399404
app._groups.add_group.return_value = group
400405

401-
conbee = application.ConBeeDevice(app, mock.sentinel.ieee, nwk)
402-
conbee.endpoints = {
406+
deconz = application.DeconzDevice(0, device_path, app, mock.sentinel.ieee, nwk)
407+
deconz.endpoints = {
403408
0: mock.sentinel.zdo,
404409
1: mock.sentinel.ep1,
405410
2: mock.sentinel.ep2,
406411
}
407412

408-
await conbee.add_to_group(mock.sentinel.grp_id, mock.sentinel.grp_name)
413+
await deconz.add_to_group(mock.sentinel.grp_id, mock.sentinel.grp_name)
409414
assert group.add_member.call_count == 2
410415

411416
assert app.groups.add_group.call_count == 1
@@ -414,33 +419,53 @@ async def test_conbee_dev_add_to_group(app, nwk):
414419

415420

416421
@pytest.mark.asyncio
417-
async def test_conbee_dev_remove_from_group(app, nwk):
422+
async def test_deconz_dev_remove_from_group(app, nwk, device_path):
418423
group = mock.MagicMock()
419424
app.groups[mock.sentinel.grp_id] = group
420-
conbee = application.ConBeeDevice(app, mock.sentinel.ieee, nwk)
421-
conbee.endpoints = {
425+
deconz = application.DeconzDevice(0, device_path, app, mock.sentinel.ieee, nwk)
426+
deconz.endpoints = {
422427
0: mock.sentinel.zdo,
423428
1: mock.sentinel.ep1,
424429
2: mock.sentinel.ep2,
425430
}
426431

427-
await conbee.remove_from_group(mock.sentinel.grp_id)
432+
await deconz.remove_from_group(mock.sentinel.grp_id)
428433
assert group.remove_member.call_count == 2
429434

430435

431-
def test_conbee_props(nwk):
432-
conbee = application.ConBeeDevice(app, mock.sentinel.ieee, nwk)
433-
assert conbee.manufacturer is not None
434-
assert conbee.model is not None
436+
def test_deconz_props(nwk, device_path):
437+
deconz = application.DeconzDevice(0, device_path, app, mock.sentinel.ieee, nwk)
438+
assert deconz.manufacturer is not None
439+
assert deconz.model is not None
440+
441+
442+
@pytest.mark.parametrize(
443+
"name, firmware_version, device_path",
444+
[
445+
("ConBee", 0x00000500, "/dev/ttyUSB0"),
446+
("ConBee II", 0x00000700, "/dev/ttyUSB0"),
447+
("RaspBee", 0x00000500, "/dev/ttyS0"),
448+
("RaspBee II", 0x00000700, "/dev/ttyS0"),
449+
("RaspBee", 0x00000500, "/dev/ttyAMA0"),
450+
("RaspBee II", 0x00000700, "/dev/ttyAMA0"),
451+
],
452+
)
453+
def test_deconz_name(nwk, name, firmware_version, device_path):
454+
deconz = application.DeconzDevice(
455+
firmware_version, device_path, app, mock.sentinel.ieee, nwk
456+
)
457+
assert deconz.model == name
435458

436459

437460
@pytest.mark.asyncio
438-
async def test_conbee_new(app, nwk, monkeypatch):
461+
async def test_deconz_new(app, nwk, device_path, monkeypatch):
439462
mock_init = mock.MagicMock(side_effect=asyncio.coroutine(mock.MagicMock()))
440463
monkeypatch.setattr(zigpy.device.Device, "_initialize", mock_init)
441464

442-
conbee = await application.ConBeeDevice.new(app, mock.sentinel.ieee, nwk)
443-
assert isinstance(conbee, application.ConBeeDevice)
465+
deconz = await application.DeconzDevice.new(
466+
app, mock.sentinel.ieee, nwk, 0, device_path
467+
)
468+
assert isinstance(deconz, application.DeconzDevice)
444469
assert mock_init.call_count == 1
445470
mock_init.reset_mock()
446471

@@ -451,8 +476,10 @@ async def test_conbee_new(app, nwk, monkeypatch):
451476
22: mock.MagicMock(),
452477
}
453478
app.devices[mock.sentinel.ieee] = mock_dev
454-
conbee = await application.ConBeeDevice.new(app, mock.sentinel.ieee, nwk)
455-
assert isinstance(conbee, application.ConBeeDevice)
479+
deconz = await application.DeconzDevice.new(
480+
app, mock.sentinel.ieee, nwk, 0, device_path
481+
)
482+
assert isinstance(deconz, application.DeconzDevice)
456483
assert mock_init.call_count == 0
457484

458485

zigpy_deconz/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22
MAJOR_VERSION = 0
33
MINOR_VERSION = 9
4-
PATCH_VERSION = "0"
4+
PATCH_VERSION = "1"
55
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
66
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)

zigpy_deconz/api.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
LOGGER = logging.getLogger(__name__)
1616

17-
COMMAND_TIMEOUT = 2
18-
PROBE_TIMEOUT = 3
17+
COMMAND_TIMEOUT = 1
18+
PROBE_TIMEOUT = 2
1919
MIN_PROTO_VERSION = 0x010B
2020

2121

@@ -204,6 +204,7 @@ def __init__(self, app: Callable, device_config: Dict[str, Any]):
204204
self._app = app
205205
self._aps_data_ind_flags: int = 0x01
206206
self._awaiting = {}
207+
self._command_lock = asyncio.Lock()
207208
self._config = device_config
208209
self._conn_lost_task: Optional[asyncio.Task] = None
209210
self._data_indication: bool = False
@@ -275,20 +276,23 @@ def close(self):
275276
self._uart = None
276277

277278
async def _command(self, cmd, *args):
278-
LOGGER.debug("Command %s %s", cmd, args)
279279
if self._uart is None:
280280
# connection was lost
281281
raise CommandError(Status.ERROR, "API is not running")
282-
data, seq = self._api_frame(cmd, *args)
283-
self._uart.send(data)
284-
fut = asyncio.Future()
285-
self._awaiting[seq] = fut
286-
try:
287-
return await asyncio.wait_for(fut, timeout=COMMAND_TIMEOUT)
288-
except asyncio.TimeoutError:
289-
LOGGER.warning("No response to '%s' command", cmd)
290-
self._awaiting.pop(seq)
291-
raise
282+
async with self._command_lock:
283+
LOGGER.debug("Command %s %s", cmd, args)
284+
data, seq = self._api_frame(cmd, *args)
285+
self._uart.send(data)
286+
fut = asyncio.Future()
287+
self._awaiting[seq] = fut
288+
try:
289+
return await asyncio.wait_for(fut, timeout=COMMAND_TIMEOUT)
290+
except asyncio.TimeoutError:
291+
LOGGER.warning(
292+
"No response to '%s' command with seq id '0x%02x'", cmd, seq
293+
)
294+
self._awaiting.pop(seq)
295+
raise
292296

293297
def _api_frame(self, cmd, *args):
294298
schema = TX_COMMANDS[cmd]

zigpy_deconz/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
CONF_DEVICE,
44
CONF_DEVICE_PATH,
55
CONFIG_SCHEMA,
6+
SCHEMA_DEVICE,
67
)
78

89
CONF_WATCHDOG_TTL = "watchdog_ttl"

zigpy_deconz/zigbee/application.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import binascii
33
import logging
4+
import re
45
from typing import Any, Dict
56

67
import zigpy.application
@@ -13,7 +14,7 @@
1314

1415
from zigpy_deconz import types as t
1516
from zigpy_deconz.api import Deconz, NetworkParameter, NetworkState, Status
16-
from zigpy_deconz.config import CONF_WATCHDOG_TTL, CONFIG_SCHEMA
17+
from zigpy_deconz.config import CONF_WATCHDOG_TTL, CONFIG_SCHEMA, SCHEMA_DEVICE
1718
import zigpy_deconz.exception
1819

1920
LOGGER = logging.getLogger(__name__)
@@ -26,6 +27,9 @@
2627

2728
class ControllerApplication(zigpy.application.ControllerApplication):
2829
SCHEMA = CONFIG_SCHEMA
30+
SCHEMA_DEVICE = SCHEMA_DEVICE
31+
32+
probe = Deconz.probe
2933

3034
def __init__(self, config: Dict[str, Any]):
3135
super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config))
@@ -73,7 +77,13 @@ async def startup(self, auto_form=False):
7377

7478
if auto_form:
7579
await self.form_network()
76-
self.devices[self.ieee] = await ConBeeDevice.new(self, self.ieee, self.nwk)
80+
self.devices[self.ieee] = await DeconzDevice.new(
81+
self,
82+
self.ieee,
83+
self.nwk,
84+
self.version,
85+
self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH],
86+
)
7787

7888
async def force_remove(self, dev):
7989
"""Forcibly remove device from NCP."""
@@ -281,9 +291,15 @@ def handle_tx_confirm(self, req_id, status):
281291
)
282292

283293

284-
class ConBeeDevice(zigpy.device.Device):
294+
class DeconzDevice(zigpy.device.Device):
285295
"""Zigpy Device representing Coordinator."""
286296

297+
def __init__(self, version: int, device_path: str, *args):
298+
super().__init__(*args)
299+
is_gpio_device = re.match(r"/dev/tty(S|AMA)\d+", device_path)
300+
self._model = "RaspBee" if is_gpio_device else "ConBee"
301+
self._model += " II" if ((version & 0x0000FF00) == 0x00000700) else ""
302+
287303
async def add_to_group(self, grp_id: int, name: str = None) -> None:
288304
group = self.application.groups.add_group(grp_id, name)
289305

@@ -306,12 +322,12 @@ def manufacturer(self):
306322

307323
@property
308324
def model(self):
309-
return "ConBee"
325+
return self._model
310326

311327
@classmethod
312-
async def new(cls, application, ieee, nwk):
328+
async def new(cls, application, ieee, nwk, version: int, device_path: str):
313329
"""Create or replace zigpy device."""
314-
dev = cls(application, ieee, nwk)
330+
dev = cls(version, device_path, application, ieee, nwk)
315331

316332
if ieee in application.devices:
317333
from_dev = application.get_device(ieee=ieee)

0 commit comments

Comments
 (0)