From acf47f6939c28b20b22d5715646e6f1a0bd70064 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 27 Nov 2021 20:00:08 -0500 Subject: [PATCH 01/11] 0.30.0.dev0 version bump --- bellows/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/__init__.py b/bellows/__init__.py index 4b07711b..84a46c57 100644 --- a/bellows/__init__.py +++ b/bellows/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 29 +MINOR_VERSION = 30 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From b5ed295b350de394e5c65c1e6d638d3348e4cb86 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sat, 29 Jan 2022 21:32:31 +0100 Subject: [PATCH 02/11] Skip get_board_info() in case EZSP Radio doesn't support it (#447) Handle exceptions in case EZSP Radio doesn't support the getMfgToken token command. This is required to make ZHA work with the EmberZNet Zigbee stack running on Linux (zigbeed). --- bellows/zigbee/application.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index c7ee6634..3e5f119d 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -145,10 +145,13 @@ async def startup(self, auto_form=False): output_clusters=[zigpy.zcl.clusters.security.IasZone.cluster_id] ) - brd_manuf, brd_name, version = await self._ezsp.get_board_info() - LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) - LOGGER.info("EZSP Radio board name: %s", brd_name) - LOGGER.info("EmberZNet version: %s", version) + try: + brd_manuf, brd_name, version = await self._ezsp.get_board_info() + LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) + LOGGER.info("EZSP Radio board name: %s", brd_name) + LOGGER.info("EmberZNet version: %s", version) + except EzspError as exc: + LOGGER.info("EZSP Radio does not support getMfgToken command: %s", str(exc)) v = await ezsp.networkInit() if v[0] != t.EmberStatus.SUCCESS: From 25b0bac81047d363e2126d0c4f1d70a05f341888 Mon Sep 17 00:00:00 2001 From: Sylvain PERON <48243214+SylvainPer@users.noreply.github.com> Date: Fri, 11 Mar 2022 14:12:52 +0100 Subject: [PATCH 03/11] Import CONF_NWK parameters to config (#451) Allows network configuration at initialization --- bellows/config/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bellows/config/__init__.py b/bellows/config/__init__.py index 2a64a063..29c609d1 100644 --- a/bellows/config/__init__.py +++ b/bellows/config/__init__.py @@ -4,6 +4,15 @@ CONF_DEVICE_PATH, CONFIG_SCHEMA, SCHEMA_DEVICE, + CONF_NWK, + CONF_NWK_KEY, + CONF_NWK_PAN_ID, + CONF_NWK_CHANNEL, + CONF_NWK_CHANNELS, + CONF_NWK_UPDATE_ID, + CONF_NWK_TC_ADDRESS, + CONF_NWK_TC_LINK_KEY, + CONF_NWK_EXTENDED_PAN_ID, cv_boolean, ) From c0a96cdba7c58a2fb8020a1308aad398c482d46a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 11 Mar 2022 13:42:28 -0500 Subject: [PATCH 04/11] Fix isort --- bellows/config/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bellows/config/__init__.py b/bellows/config/__init__.py index 29c609d1..b6dfd059 100644 --- a/bellows/config/__init__.py +++ b/bellows/config/__init__.py @@ -2,17 +2,17 @@ from zigpy.config import ( # noqa: F401 pylint: disable=unused-import CONF_DEVICE, CONF_DEVICE_PATH, - CONFIG_SCHEMA, - SCHEMA_DEVICE, CONF_NWK, - CONF_NWK_KEY, - CONF_NWK_PAN_ID, CONF_NWK_CHANNEL, CONF_NWK_CHANNELS, - CONF_NWK_UPDATE_ID, + CONF_NWK_EXTENDED_PAN_ID, + CONF_NWK_KEY, + CONF_NWK_PAN_ID, CONF_NWK_TC_ADDRESS, CONF_NWK_TC_LINK_KEY, - CONF_NWK_EXTENDED_PAN_ID, + CONF_NWK_UPDATE_ID, + CONFIG_SCHEMA, + SCHEMA_DEVICE, cv_boolean, ) From ed576570b5193e07d8fc2c6cd53801072bab9d2b Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sun, 20 Mar 2022 17:50:36 +0100 Subject: [PATCH 05/11] Add stream subcommand for RF testing (#454) * Add stream subcommand for RF testing The stream subcommmand uses the manufacturing library to transmit random characters continously. This is useful for RF testing and approvals. * Address black/flake8 errors --- bellows/cli/__init__.py | 2 +- bellows/cli/stream.py | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 bellows/cli/stream.py diff --git a/bellows/cli/__init__.py b/bellows/cli/__init__.py index b44d8087..97f60334 100644 --- a/bellows/cli/__init__.py +++ b/bellows/cli/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from . import application, backup, dump, ncp, network +from . import application, backup, dump, ncp, network, stream diff --git a/bellows/cli/stream.py b/bellows/cli/stream.py new file mode 100644 index 00000000..fea70959 --- /dev/null +++ b/bellows/cli/stream.py @@ -0,0 +1,59 @@ +import asyncio +import logging +import time + +import click + +from . import util +from .main import main + +LOGGER = logging.getLogger(__name__) + + +@main.command() +@click.option( + "-c", "--channel", type=click.IntRange(11, 26), metavar="CHANNEL", required=True +) +@click.option( + "-p", "--power", type=click.IntRange(-100, 20), metavar="POWER", required=True +) +@click.pass_context +def stream(ctx, channel, power): + """Transmit random stream of characters on CHANNEL with POWER (in dBm).""" + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(_stream(ctx, channel, power)) + except KeyboardInterrupt: + start_time = ctx.obj.get("start_time", None) + if start_time: + duration = time.time() - start_time + click.echo( + "\nStreamed on channel %d for %0.2fs" % (channel, duration), err=True + ) + finally: + if "ezsp" in ctx.obj: + s = ctx.obj["ezsp"] + loop.run_until_complete(s.mfglibStopStream()) + loop.run_until_complete(s.mfglibEnd()) + s.close() + + +async def _stream(ctx, channel, power): + s = await util.setup(ctx.obj["device"], ctx.obj["baudrate"]) + ctx.obj["ezsp"] = s + + v = await s.mfglibStart(False) + util.check(v[0], "Unable to start mfglib") + + v = await s.mfglibSetChannel(channel) + util.check(v[0], "Unable to set channel") + + v = await s.mfglibSetPower(0, power) + util.check(v[0], "Unable to set power") + + v = await s.mfglibStartStream() + util.check(v[0], "Unable to start stream") + click.echo("Started transmitting random stream of characters", err=True) + ctx.obj["start_time"] = time.time() + while True: + await asyncio.sleep(3600) From 78ae8b5256e8e917701e19cf97d0077f619c5482 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 24 Mar 2022 14:03:32 +0100 Subject: [PATCH 06/11] Set probe timeout to 3s for zigbeed (#453) The Silicon Labs zigbee daemon restarts on reset. Restarts take some time, and with a probe timeout of 2s probe often stops before the zigbeed finished restarting. This is especially noticable since Gecko SDK 4.0.2.0. --- bellows/ezsp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 5c19f8f5..89023d93 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -24,7 +24,7 @@ from . import v4, v5, v6, v7, v8 EZSP_LATEST = v8.EZSP_VERSION -PROBE_TIMEOUT = 2 +PROBE_TIMEOUT = 3 NETWORK_OPS_TIMEOUT = 10 LOGGER = logging.getLogger(__name__) MTOR_MIN_INTERVAL = 10 From c4d1f27062f522a4fd3c504428b68919669d0b22 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 6 Apr 2022 03:26:36 +0200 Subject: [PATCH 07/11] Fix black with latest click (#457) * Fix black with latest click Latest click broke black (see https://github.com/psf/black/issues/2964 for details). Use latest black to fix black in our CI environment. Also add .pre-commit-config.yaml to the key of the cache. * Fix workflow * Use prepared environment with correct key * Use prepared environment with correct key everywhere * Fix new found black issues --- .github/workflows/ci.yml | 24 ++++++++++++++++-------- .pre-commit-config.yaml | 2 +- bellows/ezsp/__init__.py | 2 +- bellows/thread.py | 2 +- bellows/types/named.py | 2 +- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8744c795..40badf3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} restore-keys: | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- - name: Create Python virtual environment @@ -69,7 +70,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -111,7 +113,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -155,7 +158,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -202,7 +206,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -246,7 +251,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -297,7 +303,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -360,7 +367,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9909b758..57e8be76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black args: diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 89023d93..14b32e5c 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -77,7 +77,7 @@ async def _probe(self) -> None: @classmethod async def initialize(cls, zigpy_config: Dict) -> "EZSP": - """Return initialized EZSP instance. """ + """Return initialized EZSP instance.""" ezsp = cls(zigpy_config[CONF_DEVICE]) await ezsp.connect() await ezsp.reset() diff --git a/bellows/thread.py b/bellows/thread.py index d9e1c939..9c25f2f4 100644 --- a/bellows/thread.py +++ b/bellows/thread.py @@ -8,7 +8,7 @@ class EventLoopThread: - """ Run a parallel event loop in a separate thread """ + """Run a parallel event loop in a separate thread.""" def __init__(self): self.loop = None diff --git a/bellows/types/named.py b/bellows/types/named.py index 6e3c11be..30ea9a9e 100644 --- a/bellows/types/named.py +++ b/bellows/types/named.py @@ -1141,7 +1141,7 @@ class EzspSourceRouteOverheadInformation(basic.enum8): class EmberKeyData(basic.fixed_list(16, basic.uint8_t)): - """A 128-bit key. """ + """A 128-bit key.""" class EmberCertificateData(basic.fixed_list(48, basic.uint8_t)): From e2e589fc00a758b4371df291cfeea7adf71a5f87 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 7 Apr 2022 00:35:00 +0200 Subject: [PATCH 08/11] Add tone command for RF testing (#456) * Add tone command for RF testing From the mfglib description: [...] start a TX tone that can be used to measure the PA output power, phase noise, frequency offset and spurious emissions using a spectrum analyzer. The TX tone is a continuous unmodulated transmission. * Fix spelling --- bellows/cli/__init__.py | 2 +- bellows/cli/tone.py | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 bellows/cli/tone.py diff --git a/bellows/cli/__init__.py b/bellows/cli/__init__.py index 97f60334..1c8f3836 100644 --- a/bellows/cli/__init__.py +++ b/bellows/cli/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from . import application, backup, dump, ncp, network, stream +from . import application, backup, dump, ncp, network, stream, tone diff --git a/bellows/cli/tone.py b/bellows/cli/tone.py new file mode 100644 index 00000000..e31e3a0c --- /dev/null +++ b/bellows/cli/tone.py @@ -0,0 +1,59 @@ +import asyncio +import logging +import time + +import click + +from . import util +from .main import main + +LOGGER = logging.getLogger(__name__) + + +@main.command() +@click.option( + "-c", "--channel", type=click.IntRange(11, 26), metavar="CHANNEL", required=True +) +@click.option( + "-p", "--power", type=click.IntRange(-100, 20), metavar="POWER", required=True +) +@click.pass_context +def tone(ctx, channel, power): + """Transmit continuous unmodulated tone on CHANNEL with POWER (in dBm).""" + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(_tone(ctx, channel, power)) + except KeyboardInterrupt: + start_time = ctx.obj.get("start_time", None) + if start_time: + duration = time.time() - start_time + click.echo( + "\nStreamed on channel %d for %0.2fs" % (channel, duration), err=True + ) + finally: + if "ezsp" in ctx.obj: + s = ctx.obj["ezsp"] + loop.run_until_complete(s.mfglibStopTone()) + loop.run_until_complete(s.mfglibEnd()) + s.close() + + +async def _tone(ctx, channel, power): + s = await util.setup(ctx.obj["device"], ctx.obj["baudrate"]) + ctx.obj["ezsp"] = s + + v = await s.mfglibStart(False) + util.check(v[0], "Unable to start mfglib") + + v = await s.mfglibSetChannel(channel) + util.check(v[0], "Unable to set channel") + + v = await s.mfglibSetPower(0, power) + util.check(v[0], "Unable to set power") + + v = await s.mfglibStartTone() + util.check(v[0], "Unable to start tone") + click.echo("Started transmitting unmodulated tone", err=True) + ctx.obj["start_time"] = time.time() + while True: + await asyncio.sleep(3600) From ad1ea565b138ad27d567184af2bcf086910011f8 Mon Sep 17 00:00:00 2001 From: sofiboselli <85589892+sofiboselli@users.noreply.github.com> Date: Sat, 30 Apr 2022 18:24:11 -0300 Subject: [PATCH 09/11] Update application.py (#455) --- bellows/cli/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/cli/application.py b/bellows/cli/application.py index aab619b3..36d85f74 100644 --- a/bellows/cli/application.py +++ b/bellows/cli/application.py @@ -200,7 +200,7 @@ async def bind(ctx, endpoint, cluster): return try: - v = await dev.zdo.bind(endpoint, cluster) + v = await dev.zdo.bind(clust) click.echo(v) except zigpy.exceptions.ZigbeeException as e: click.echo(e) From 0b3d934a6530216b87866e99bd60ea3782c101b2 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 10 May 2022 22:50:27 +0200 Subject: [PATCH 10/11] Fix UART port close on RSTACK message during startup (#459) * Fix UART port close on RSTACK message during startup When a RSTACK message is processed right after the UART has been opened, it causes EZSP.enter_failed_state() getting called at a point where the application callbacks are not registered yet. In that case the UART will get closed and it won't get opened again. Bellows is stuck with a closed transport. Avoid this issue by not closing the port in case there is no application callback registered yet. Typically, it is unlikely that a RSTACK message arrives right when the port gets opened (the race window is very narrow). However, with hardware flow control opening the port leads to RTS signal to get asserted which causes the radio to send pending messages, e.g. resets caused by EmberZNet watchdog. Note: With hardware flow control this is only the case if the tty "hupcl" option is set. The option is set by default, but cleared by tools like GNU screen. This option makes sure that the RTS signal is deasserted while the port is closed. Pyserial/bellows does not change the state of that option. * Fix tests * Address black * Check that close is not called without callbacks registered * Address black again --- bellows/ezsp/__init__.py | 11 ++++++++--- tests/test_ezsp.py | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 14b32e5c..c07cf397 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -219,9 +219,14 @@ def connection_lost(self, exc): def enter_failed_state(self, error): """UART received error frame.""" - LOGGER.error("NCP entered failed state. Requesting APP controller restart") - self.close() - self.handle_callback("_reset_controller_application", (error,)) + if self._callbacks: + LOGGER.error("NCP entered failed state. Requesting APP controller restart") + self.close() + self.handle_callback("_reset_controller_application", (error,)) + else: + LOGGER.info( + "NCP entered failed state. No application handler registered, ignoring..." + ) def __getattr__(self, name: str) -> Callable: if name not in self._protocol.COMMANDS: diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 23ac5b36..77b2d252 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -241,12 +241,22 @@ def test_connection_lost(ezsp_f): async def test_enter_failed_state(ezsp_f): ezsp_f.stop_ezsp = MagicMock(spec_set=ezsp_f.stop_ezsp) - ezsp_f.handle_callback = MagicMock(spec_set=ezsp_f.handle_callback) + cb = MagicMock(spec_set=ezsp_f.handle_callback) + ezsp_f.add_callback(cb) ezsp_f.enter_failed_state(sentinel.error) await asyncio.sleep(0) assert ezsp_f.stop_ezsp.call_count == 1 - assert ezsp_f.handle_callback.call_count == 1 - assert ezsp_f.handle_callback.call_args[0][1][0] == sentinel.error + assert cb.call_count == 1 + assert cb.call_args[0][1][0] == sentinel.error + + +async def test_no_close_without_callback(ezsp_f): + ezsp_f.stop_ezsp = MagicMock(spec_set=ezsp_f.stop_ezsp) + ezsp_f.close = MagicMock(spec_set=ezsp_f.close) + ezsp_f.enter_failed_state(sentinel.error) + await asyncio.sleep(0) + assert ezsp_f.stop_ezsp.call_count == 0 + assert ezsp_f.close.call_count == 0 @patch.object(ezsp.EZSP, "reset", new_callable=AsyncMock) From a0de294b6c2fc3b47271d3113a64c090fc387dc6 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 10 May 2022 16:53:08 -0400 Subject: [PATCH 11/11] 0.30.0 version bump --- bellows/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/__init__.py b/bellows/__init__.py index 84a46c57..97994f1a 100644 --- a/bellows/__init__.py +++ b/bellows/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 MINOR_VERSION = 30 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"