Skip to content

Commit e8b644f

Browse files
authored
0.12.0 Release
2 parents 6ad2a12 + 2c6409f commit e8b644f

File tree

9 files changed

+315
-23
lines changed

9 files changed

+315
-23
lines changed

Contributors.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Contributors
2+
- [Russell Cloran] (https://github.com/rcloran)
3+
- [Alexei Chetroi] (https://github.com/Adminiuga)
4+
- [Andreas Bomholtz] (https://github.com/AndreasBomholtz)
5+
- [damarco] (https://github.com/damarco)
6+
- [Kyle Hendricks] (https://github.com/kylehendricks)
7+
- [Yoda-x] (https://github.com/Yoda-x)
8+
- [Elelabs-maintainer] (https://github.com/Elelabs-maintainer)
9+
- [Peter Marheine] (https://github.com/tari)
10+
- [Matthew Schick] (https://github.com/mattsch)
11+
- [henryptung] (https://github.com/henryptung)
12+
- [Gavin Mogan] (https://github.com/halkeye)
13+
- [h3ndrik] (https://github.com/h3ndrik)
14+
- [Hedda] (https://github.com/Hedda)

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,11 @@ Packages of tagged versions are also released via PyPI
7070
https://www.silabs.com/Support%20Documents/TechnicalDocs/UG101.pdf
7171
* EZSP Reference Guide:
7272
http://www.silabs.com/Support%20Documents/TechnicalDocs/UG100-EZSPReferenceGuide.pdf
73+
74+
## How to contribute
75+
76+
If you are looking to make a contribution to this project we suggest that you follow the steps in these guides:
77+
- https://github.com/firstcontributions/first-contributions/blob/master/README.md
78+
- https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md
79+
80+
Some developers might also be interested in receiving donations in the form of hardware such as Zigbee modules or devices, and even if such donations are most often donated with no strings attached it could in many cases help the developers motivation and indirect improve the development of this project.

bellows/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
MAJOR_VERSION = 0
2-
MINOR_VERSION = 11
2+
MINOR_VERSION = 12
33
PATCH_VERSION = "0"
44
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
55
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)

bellows/zigbee/application.py

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,30 @@
22
import logging
33
import os
44

5+
from bellows.exception import ControllerError, EzspError
6+
import bellows.multicast
7+
import bellows.types as t
8+
import bellows.zigbee.util
59
from serial import SerialException
6-
from zigpy.quirks import CustomDevice, CustomEndpoint
7-
from zigpy.types import BroadcastAddress
10+
import voluptuous as vol
811
import zigpy.application
912
import zigpy.device
13+
from zigpy.quirks import CustomDevice, CustomEndpoint
14+
from zigpy.types import BroadcastAddress
1015
import zigpy.util
1116
import zigpy.zdo
1217
import zigpy.zdo.types as zdo_t
1318

14-
import bellows.types as t
15-
import bellows.zigbee.util
16-
from bellows.exception import ControllerError, EzspError
17-
import bellows.multicast
18-
1919
APS_ACK_TIMEOUT = 120
20+
CONF_PARAM_SRC_RTG = "source_routing"
21+
CONFIG_SCHEMA = zigpy.application.CONFIG_SCHEMA.extend(
22+
{vol.Optional(CONF_PARAM_SRC_RTG, default=False): bellows.zigbee.util.cv_boolean}
23+
)
2024
EZSP_DEFAULT_RADIUS = 0
2125
EZSP_MULTICAST_NON_MEMBER_RADIUS = 3
2226
MAX_WATCHDOG_FAILURES = 4
27+
MTOR_MIN_INTERVAL = 600
28+
MTOR_MAX_INTERVAL = 1800
2329
RESET_ATTEMPT_BACKOFF_TIME = 5
2430
WATCHDOG_WAKE_PERIOD = 10
2531

@@ -29,15 +35,20 @@
2935
class ControllerApplication(zigpy.application.ControllerApplication):
3036
direct = t.EmberOutgoingMessageType.OUTGOING_DIRECT
3137

32-
def __init__(self, ezsp, database_file=None):
33-
super().__init__(database_file=database_file)
38+
def __init__(self, ezsp, database_file=None, config={}):
39+
super().__init__(database_file=database_file, config=CONFIG_SCHEMA(config))
3440
self._ctrl_event = asyncio.Event()
3541
self._ezsp = ezsp
3642
self._multicast = bellows.multicast.Multicast(ezsp)
3743
self._pending = zigpy.util.Requests()
3844
self._watchdog_task = None
3945
self._reset_task = None
4046
self._in_flight_msg = None
47+
self._tx_options = t.EmberApsOption(
48+
t.EmberApsOption.APS_OPTION_RETRY
49+
| t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY
50+
)
51+
self.use_source_routing = self.config[CONF_PARAM_SRC_RTG]
4152

4253
@property
4354
def controller_event(self):
@@ -124,6 +135,7 @@ async def startup(self, auto_form=False):
124135
await self.initialize()
125136
e = self._ezsp
126137

138+
await self.set_source_routing()
127139
v = await e.networkInit()
128140
if v[0] != t.EmberStatus.SUCCESS:
129141
if not auto_form:
@@ -153,8 +165,25 @@ async def startup(self, auto_form=False):
153165

154166
self.handle_join(self.nwk, self.ieee, 0)
155167
LOGGER.debug("EZSP nwk=0x%04x, IEEE=%s", self._nwk, str(self._ieee))
168+
156169
await self.multicast.startup(self.get_device(self.ieee))
157170

171+
async def set_source_routing(self) -> None:
172+
res = await self._ezsp.setConcentrator(
173+
self.use_source_routing,
174+
t.EmberConcentratorType.HIGH_RAM_CONCENTRATOR,
175+
MTOR_MIN_INTERVAL,
176+
MTOR_MAX_INTERVAL,
177+
2,
178+
5,
179+
0,
180+
)
181+
LOGGER.debug("Set concentrator type: %s", res)
182+
if res[0] != t.EmberStatus.SUCCESS:
183+
LOGGER.warning(
184+
"Couldn't set concentrator type %s: %s", self.use_source_routing, res
185+
)
186+
158187
async def shutdown(self):
159188
"""Shutdown and cleanup ControllerApplication."""
160189
LOGGER.info("Shutting down ControllerApplication")
@@ -235,6 +264,10 @@ def ezsp_callback_handler(self, frame_name, args):
235264
self.handle_leave(args[0], args[1])
236265
else:
237266
self.handle_join(args[0], args[1], args[4])
267+
elif frame_name == "incomingRouteRecordHandler":
268+
self.handle_route_record(*args)
269+
elif frame_name == "incomingRouteErrorHandler":
270+
self.handle_route_error(*args)
238271
elif frame_name == "_reset_controller_application":
239272
self._handle_reset_request(*args)
240273

@@ -432,10 +465,7 @@ async def request(
432465
aps_frame.clusterId = t.uint16_t(cluster)
433466
aps_frame.sourceEndpoint = t.uint8_t(src_ep)
434467
aps_frame.destinationEndpoint = t.uint8_t(dst_ep)
435-
aps_frame.options = t.EmberApsOption(
436-
t.EmberApsOption.APS_OPTION_RETRY
437-
| t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY
438-
)
468+
aps_frame.options = self._tx_options
439469
aps_frame.groupId = t.uint16_t(0)
440470
aps_frame.sequence = t.uint8_t(sequence)
441471
message_tag = self.get_sequence()
@@ -444,11 +474,30 @@ async def request(
444474
LOGGER.warning(
445475
("EUI64 addressing is not currently supported, " "reverting to NWK")
446476
)
447-
if expect_reply and device.node_desc.is_end_device in (True, None):
448-
LOGGER.debug("Extending timeout for %s/0x%04x", device.ieee, device.nwk)
449-
await self._ezsp.setExtendedTimeout(device.ieee, True)
450477
with self._pending.new(message_tag) as req:
451478
async with self._in_flight_msg:
479+
if expect_reply and device.node_desc.is_end_device in (True, None):
480+
LOGGER.debug(
481+
"Extending timeout for %s/0x%04x", device.ieee, device.nwk
482+
)
483+
await self._ezsp.setExtendedTimeout(device.ieee, True)
484+
if self.use_source_routing and device.relays is not None:
485+
res = await self._ezsp.setSourceRoute(device.nwk, device.relays)
486+
if res[0] != t.EmberStatus.SUCCESS:
487+
LOGGER.warning(
488+
"Couldn't set source route for %s: %s", device.nwk, res
489+
)
490+
else:
491+
aps_frame.options = t.EmberApsOption(
492+
aps_frame.options
493+
^ t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY
494+
)
495+
LOGGER.debug(
496+
"Set source route for %s to %s: %s",
497+
device.nwk,
498+
device.relays,
499+
res,
500+
)
452501
res = await self._ezsp.sendUnicast(
453502
self.direct, device.nwk, aps_frame, message_tag, data
454503
)
@@ -563,6 +612,34 @@ async def _watchdog(self):
563612
"Watchdog timeout. Heartbeat timeouts: {}".format(failures)
564613
)
565614

615+
def handle_route_record(
616+
self,
617+
nwk: t.EmberNodeId,
618+
ieee: t.EmberEUI64,
619+
lqi: t.uint8_t,
620+
rssi: t.int8s,
621+
relays: t.LVList(t.EmberNodeId),
622+
) -> None:
623+
LOGGER.debug(
624+
"Processing route record request: %s", (nwk, ieee, lqi, rssi, relays)
625+
)
626+
try:
627+
dev = self.get_device(ieee=ieee)
628+
except KeyError:
629+
LOGGER.debug("Why we don't have a device for %s ieee and %s NWK", ieee, nwk)
630+
self.handle_join(nwk, ieee, 0)
631+
return
632+
dev.relays = relays
633+
634+
def handle_route_error(self, status: t.EmberStatus, nwk: t.EmberNodeId) -> None:
635+
LOGGER.debug("Processing route error: status=%s, nwk=%s", status, nwk)
636+
try:
637+
dev = self.get_device(nwk=nwk)
638+
except KeyError:
639+
LOGGER.debug("No %s device found", nwk)
640+
return
641+
dev.relays = None
642+
566643

567644
class EZSPCoordinator(CustomDevice):
568645
"""Zigpy Device representing Coordinator."""

bellows/zigbee/util.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2+
from typing import Any
23

34
import bellows.types as t
5+
import voluptuous as vol
46

57

68
def zha_security(controller=False):
@@ -30,3 +32,18 @@ def zha_security(controller=False):
3032
random_key = t.fixed_list(16, t.uint8_t)([t.uint8_t(x) for x in os.urandom(16)])
3133
isc.networkKey = random_key
3234
return isc
35+
36+
37+
def cv_boolean(value: Any) -> bool:
38+
"""Validate and coerce a boolean value."""
39+
if isinstance(value, bool):
40+
return value
41+
if isinstance(value, str):
42+
value = value.lower().strip()
43+
if value in ("1", "true", "yes", "on", "enable"):
44+
return True
45+
if value in ("0", "false", "no", "off", "disable"):
46+
return False
47+
elif isinstance(value, int):
48+
return bool(value)
49+
raise vol.Invalid("invalid boolean value {}".format(value))

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
"click-log==0.2.0",
2020
"pure_pcapy3==1.0.1",
2121
"pyserial-asyncio",
22-
"zigpy-homeassistant>=0.9.0",
22+
"voluptuous",
23+
"zigpy-homeassistant>=0.12.0",
2324
],
2425
dependency_links=["https://codeload.github.com/rcloran/pure-pcapy-3/zip/master"],
25-
tests_require=["pytest"],
26+
tests_require=["asynctest", "pytest", "pytest-asyncio"],
2627
)

tests/test_app_util.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import bellows.zigbee.util
2+
import pytest
3+
import voluptuous as vol
4+
5+
6+
@pytest.mark.parametrize(
7+
"value, result",
8+
[
9+
(False, False),
10+
(True, True),
11+
("1", True),
12+
("yes", True),
13+
("YeS", True),
14+
("on", True),
15+
("oN", True),
16+
("enable", True),
17+
("enablE", True),
18+
(0, False),
19+
("no", False),
20+
("nO", False),
21+
("off", False),
22+
("ofF", False),
23+
("disable", False),
24+
("disablE", False),
25+
],
26+
)
27+
def test_config_validation_bool(value, result):
28+
"""Test boolean config validation."""
29+
assert bellows.zigbee.util.cv_boolean(value) is result
30+
31+
32+
@pytest.mark.parametrize("value", ["invalid", "not a bool", "something"])
33+
def test_config_validation_bool_invalid(value):
34+
"""Test boolean config validation."""
35+
with pytest.raises(vol.Invalid):
36+
bellows.zigbee.util.cv_boolean(value)

0 commit comments

Comments
 (0)