Skip to content

Commit

Permalink
0.12.0 Release
Browse files Browse the repository at this point in the history
  • Loading branch information
Adminiuga authored Jan 29, 2020
2 parents 6ad2a12 + 2c6409f commit e8b644f
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 23 deletions.
14 changes: 14 additions & 0 deletions Contributors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Contributors
- [Russell Cloran] (https://github.com/rcloran)
- [Alexei Chetroi] (https://github.com/Adminiuga)
- [Andreas Bomholtz] (https://github.com/AndreasBomholtz)
- [damarco] (https://github.com/damarco)
- [Kyle Hendricks] (https://github.com/kylehendricks)
- [Yoda-x] (https://github.com/Yoda-x)
- [Elelabs-maintainer] (https://github.com/Elelabs-maintainer)
- [Peter Marheine] (https://github.com/tari)
- [Matthew Schick] (https://github.com/mattsch)
- [henryptung] (https://github.com/henryptung)
- [Gavin Mogan] (https://github.com/halkeye)
- [h3ndrik] (https://github.com/h3ndrik)
- [Hedda] (https://github.com/Hedda)
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,11 @@ Packages of tagged versions are also released via PyPI
https://www.silabs.com/Support%20Documents/TechnicalDocs/UG101.pdf
* EZSP Reference Guide:
http://www.silabs.com/Support%20Documents/TechnicalDocs/UG100-EZSPReferenceGuide.pdf

## How to contribute

If you are looking to make a contribution to this project we suggest that you follow the steps in these guides:
- https://github.com/firstcontributions/first-contributions/blob/master/README.md
- https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md

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.
2 changes: 1 addition & 1 deletion bellows/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MAJOR_VERSION = 0
MINOR_VERSION = 11
MINOR_VERSION = 12
PATCH_VERSION = "0"
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)
109 changes: 93 additions & 16 deletions bellows/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@
import logging
import os

from bellows.exception import ControllerError, EzspError
import bellows.multicast
import bellows.types as t
import bellows.zigbee.util
from serial import SerialException
from zigpy.quirks import CustomDevice, CustomEndpoint
from zigpy.types import BroadcastAddress
import voluptuous as vol
import zigpy.application
import zigpy.device
from zigpy.quirks import CustomDevice, CustomEndpoint
from zigpy.types import BroadcastAddress
import zigpy.util
import zigpy.zdo
import zigpy.zdo.types as zdo_t

import bellows.types as t
import bellows.zigbee.util
from bellows.exception import ControllerError, EzspError
import bellows.multicast

APS_ACK_TIMEOUT = 120
CONF_PARAM_SRC_RTG = "source_routing"
CONFIG_SCHEMA = zigpy.application.CONFIG_SCHEMA.extend(
{vol.Optional(CONF_PARAM_SRC_RTG, default=False): bellows.zigbee.util.cv_boolean}
)
EZSP_DEFAULT_RADIUS = 0
EZSP_MULTICAST_NON_MEMBER_RADIUS = 3
MAX_WATCHDOG_FAILURES = 4
MTOR_MIN_INTERVAL = 600
MTOR_MAX_INTERVAL = 1800
RESET_ATTEMPT_BACKOFF_TIME = 5
WATCHDOG_WAKE_PERIOD = 10

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

def __init__(self, ezsp, database_file=None):
super().__init__(database_file=database_file)
def __init__(self, ezsp, database_file=None, config={}):
super().__init__(database_file=database_file, config=CONFIG_SCHEMA(config))
self._ctrl_event = asyncio.Event()
self._ezsp = ezsp
self._multicast = bellows.multicast.Multicast(ezsp)
self._pending = zigpy.util.Requests()
self._watchdog_task = None
self._reset_task = None
self._in_flight_msg = None
self._tx_options = t.EmberApsOption(
t.EmberApsOption.APS_OPTION_RETRY
| t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY
)
self.use_source_routing = self.config[CONF_PARAM_SRC_RTG]

@property
def controller_event(self):
Expand Down Expand Up @@ -124,6 +135,7 @@ async def startup(self, auto_form=False):
await self.initialize()
e = self._ezsp

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

self.handle_join(self.nwk, self.ieee, 0)
LOGGER.debug("EZSP nwk=0x%04x, IEEE=%s", self._nwk, str(self._ieee))

await self.multicast.startup(self.get_device(self.ieee))

async def set_source_routing(self) -> None:
res = await self._ezsp.setConcentrator(
self.use_source_routing,
t.EmberConcentratorType.HIGH_RAM_CONCENTRATOR,
MTOR_MIN_INTERVAL,
MTOR_MAX_INTERVAL,
2,
5,
0,
)
LOGGER.debug("Set concentrator type: %s", res)
if res[0] != t.EmberStatus.SUCCESS:
LOGGER.warning(
"Couldn't set concentrator type %s: %s", self.use_source_routing, res
)

async def shutdown(self):
"""Shutdown and cleanup ControllerApplication."""
LOGGER.info("Shutting down ControllerApplication")
Expand Down Expand Up @@ -235,6 +264,10 @@ def ezsp_callback_handler(self, frame_name, args):
self.handle_leave(args[0], args[1])
else:
self.handle_join(args[0], args[1], args[4])
elif frame_name == "incomingRouteRecordHandler":
self.handle_route_record(*args)
elif frame_name == "incomingRouteErrorHandler":
self.handle_route_error(*args)
elif frame_name == "_reset_controller_application":
self._handle_reset_request(*args)

Expand Down Expand Up @@ -432,10 +465,7 @@ async def request(
aps_frame.clusterId = t.uint16_t(cluster)
aps_frame.sourceEndpoint = t.uint8_t(src_ep)
aps_frame.destinationEndpoint = t.uint8_t(dst_ep)
aps_frame.options = t.EmberApsOption(
t.EmberApsOption.APS_OPTION_RETRY
| t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY
)
aps_frame.options = self._tx_options
aps_frame.groupId = t.uint16_t(0)
aps_frame.sequence = t.uint8_t(sequence)
message_tag = self.get_sequence()
Expand All @@ -444,11 +474,30 @@ async def request(
LOGGER.warning(
("EUI64 addressing is not currently supported, " "reverting to NWK")
)
if expect_reply and device.node_desc.is_end_device in (True, None):
LOGGER.debug("Extending timeout for %s/0x%04x", device.ieee, device.nwk)
await self._ezsp.setExtendedTimeout(device.ieee, True)
with self._pending.new(message_tag) as req:
async with self._in_flight_msg:
if expect_reply and device.node_desc.is_end_device in (True, None):
LOGGER.debug(
"Extending timeout for %s/0x%04x", device.ieee, device.nwk
)
await self._ezsp.setExtendedTimeout(device.ieee, True)
if self.use_source_routing and device.relays is not None:
res = await self._ezsp.setSourceRoute(device.nwk, device.relays)
if res[0] != t.EmberStatus.SUCCESS:
LOGGER.warning(
"Couldn't set source route for %s: %s", device.nwk, res
)
else:
aps_frame.options = t.EmberApsOption(
aps_frame.options
^ t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY
)
LOGGER.debug(
"Set source route for %s to %s: %s",
device.nwk,
device.relays,
res,
)
res = await self._ezsp.sendUnicast(
self.direct, device.nwk, aps_frame, message_tag, data
)
Expand Down Expand Up @@ -563,6 +612,34 @@ async def _watchdog(self):
"Watchdog timeout. Heartbeat timeouts: {}".format(failures)
)

def handle_route_record(
self,
nwk: t.EmberNodeId,
ieee: t.EmberEUI64,
lqi: t.uint8_t,
rssi: t.int8s,
relays: t.LVList(t.EmberNodeId),
) -> None:
LOGGER.debug(
"Processing route record request: %s", (nwk, ieee, lqi, rssi, relays)
)
try:
dev = self.get_device(ieee=ieee)
except KeyError:
LOGGER.debug("Why we don't have a device for %s ieee and %s NWK", ieee, nwk)
self.handle_join(nwk, ieee, 0)
return
dev.relays = relays

def handle_route_error(self, status: t.EmberStatus, nwk: t.EmberNodeId) -> None:
LOGGER.debug("Processing route error: status=%s, nwk=%s", status, nwk)
try:
dev = self.get_device(nwk=nwk)
except KeyError:
LOGGER.debug("No %s device found", nwk)
return
dev.relays = None


class EZSPCoordinator(CustomDevice):
"""Zigpy Device representing Coordinator."""
Expand Down
17 changes: 17 additions & 0 deletions bellows/zigbee/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
from typing import Any

import bellows.types as t
import voluptuous as vol


def zha_security(controller=False):
Expand Down Expand Up @@ -30,3 +32,18 @@ def zha_security(controller=False):
random_key = t.fixed_list(16, t.uint8_t)([t.uint8_t(x) for x in os.urandom(16)])
isc.networkKey = random_key
return isc


def cv_boolean(value: Any) -> bool:
"""Validate and coerce a boolean value."""
if isinstance(value, bool):
return value
if isinstance(value, str):
value = value.lower().strip()
if value in ("1", "true", "yes", "on", "enable"):
return True
if value in ("0", "false", "no", "off", "disable"):
return False
elif isinstance(value, int):
return bool(value)
raise vol.Invalid("invalid boolean value {}".format(value))
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
"click-log==0.2.0",
"pure_pcapy3==1.0.1",
"pyserial-asyncio",
"zigpy-homeassistant>=0.9.0",
"voluptuous",
"zigpy-homeassistant>=0.12.0",
],
dependency_links=["https://codeload.github.com/rcloran/pure-pcapy-3/zip/master"],
tests_require=["pytest"],
tests_require=["asynctest", "pytest", "pytest-asyncio"],
)
36 changes: 36 additions & 0 deletions tests/test_app_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import bellows.zigbee.util
import pytest
import voluptuous as vol


@pytest.mark.parametrize(
"value, result",
[
(False, False),
(True, True),
("1", True),
("yes", True),
("YeS", True),
("on", True),
("oN", True),
("enable", True),
("enablE", True),
(0, False),
("no", False),
("nO", False),
("off", False),
("ofF", False),
("disable", False),
("disablE", False),
],
)
def test_config_validation_bool(value, result):
"""Test boolean config validation."""
assert bellows.zigbee.util.cv_boolean(value) is result


@pytest.mark.parametrize("value", ["invalid", "not a bool", "something"])
def test_config_validation_bool_invalid(value):
"""Test boolean config validation."""
with pytest.raises(vol.Invalid):
bellows.zigbee.util.cv_boolean(value)
Loading

0 comments on commit e8b644f

Please sign in to comment.