From bc5f9ec11814465b98c404c91ad4350225a90b6b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 14 Nov 2021 11:48:54 -0500 Subject: [PATCH 01/16] Initial implementation of cross-adapter network backup/restore --- zigpy_cli/radio.py | 52 +++++++++++++++++++++++++++++----------------- zigpy_cli/utils.py | 5 ----- 2 files changed, 33 insertions(+), 24 deletions(-) delete mode 100644 zigpy_cli/utils.py diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index f7b257b..bef92ba 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -1,5 +1,6 @@ from __future__ import annotations +import pickle import logging import importlib @@ -7,7 +8,6 @@ import zigpy.config as conf from zigpy_cli.cli import cli, click_coroutine -from zigpy_cli.utils import format_bytes from zigpy_cli.common import RADIO_TO_PYPI, RADIO_TO_PACKAGE, RADIO_LOGGING_CONFIGS LOGGER = logging.getLogger(__name__) @@ -49,6 +49,8 @@ async def radio(ctx, radio, port): ) app = app_cls(config) + await app.connect() + ctx.obj = app ctx.call_on_close(radio_cleanup) @@ -57,36 +59,49 @@ async def radio(ctx, radio, port): @click_coroutine async def radio_cleanup(app): try: - await app.pre_shutdown() + await app.shutdown() except RuntimeError: LOGGER.warning("Caught an exception when shutting down app", exc_info=True) -def dump_app_info(app): - if app.pan_id is not None: - print(f"PAN ID: 0x{app.pan_id:04X}") +@radio.command() +@click.pass_obj +@click_coroutine +async def info(app): + await app.load_network_info(load_devices=False) - print(f"Extended PAN ID: {app.extended_pan_id}") - print(f"Channel: {app.channel}") + print(f"PAN ID: 0x{app.state.network_info.pan_id:04X}") + print(f"Extended PAN ID: {app.state.network_info.extended_pan_id}") + print(f"Channel: {app.state.network_info.channel}") + print(f"Channel mask: {list(app.state.network_info.channel_mask)}") + print(f"NWK update ID: {app.state.network_info.nwk_update_id}") + print(f"Device IEEE: {app.state.node_info.ieee}") + print(f"Device NWK: 0x{app.state.node_info.nwk:04X}") + print(f"Network key: {app.state.network_info.network_key.key}") + print(f"Network key sequence: {app.state.network_info.network_key.seq}") + print(f"Network key counter: {app.state.network_info.network_key.tx_counter}") - if app.channels is not None: - print(f"Channel mask: {list(app.channels)}") - print(f"NWK update ID: {app.nwk_update_id}") - print(f"Device IEEE: {app.ieee}") - print(f"Device NWK: 0x{app.nwk:04X}") +@radio.command() +@click.argument("output", type=click.File("wb")) +@click.pass_obj +@click_coroutine +async def backup(app, output): + await app.load_network_info(load_devices=True) - if getattr(app, "network_key", None) is not None: - print(f"Network key: {format_bytes(app.network_key)}") - print(f"Network key sequence: {app.network_key_seq}") + # TODO: switch to JSON serialization for security + pickle.dump((app.state.network_info, app.state.node_info), output) @radio.command() +@click.argument("input", type=click.File("rb")) @click.pass_obj @click_coroutine -async def info(app): - await app.startup(auto_form=False) - dump_app_info(app) +async def restore(app, input): + network_info, node_info = pickle.load(input) + + # TODO: switch to JSON serialization for security + await app.write_network_info(network_info=network_info, node_info=node_info) @radio.command() @@ -95,4 +110,3 @@ async def info(app): async def form(app): await app.startup(auto_form=True) await app.form_network() - dump_app_info(app) diff --git a/zigpy_cli/utils.py b/zigpy_cli/utils.py deleted file mode 100644 index c474efc..0000000 --- a/zigpy_cli/utils.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - - -def format_bytes(data: bytes) -> str: - return ":".join(f"{b:02x}" for b in data.serialize()) From 688a79310273575e3e181b2eb8589faa432f2c3f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 28 Nov 2021 18:32:44 -0500 Subject: [PATCH 02/16] Use new zigpy JSON state serialization format --- zigpy_cli/radio.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index bef92ba..5f54c04 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -1,10 +1,11 @@ from __future__ import annotations -import pickle +import json import logging import importlib import click +import zigpy.state import zigpy.config as conf from zigpy_cli.cli import cli, click_coroutine @@ -49,8 +50,6 @@ async def radio(ctx, radio, port): ) app = app_cls(config) - await app.connect() - ctx.obj = app ctx.call_on_close(radio_cleanup) @@ -68,6 +67,7 @@ async def radio_cleanup(app): @click.pass_obj @click_coroutine async def info(app): + await app.connect() await app.load_network_info(load_devices=False) print(f"PAN ID: 0x{app.state.network_info.pan_id:04X}") @@ -83,24 +83,31 @@ async def info(app): @radio.command() -@click.argument("output", type=click.File("wb")) +@click.argument("output", type=click.File("w")) @click.pass_obj @click_coroutine async def backup(app, output): + await app.connect() await app.load_network_info(load_devices=True) - # TODO: switch to JSON serialization for security - pickle.dump((app.state.network_info, app.state.node_info), output) + obj = zigpy.state.network_state_to_json( + network_info=app.state.network_info, + node_info=app.state.node_info, + source="zigpy-cli@0.0.1", + ) + + output.write(json.dumps(obj, indent=4)) @radio.command() -@click.argument("input", type=click.File("rb")) +@click.argument("input", type=click.File("r")) @click.pass_obj @click_coroutine async def restore(app, input): - network_info, node_info = pickle.load(input) + obj = json.load(input) + network_info, node_info = zigpy.state.json_to_network_state(obj) - # TODO: switch to JSON serialization for security + await app.connect() await app.write_network_info(network_info=network_info, node_info=node_info) @@ -108,5 +115,6 @@ async def restore(app, input): @click.pass_obj @click_coroutine async def form(app): + await app.connect() await app.startup(auto_form=True) await app.form_network() From 00bb7fce083900aee8f8c18c3bce2979b0b3836e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 28 Nov 2021 18:33:07 -0500 Subject: [PATCH 03/16] Increment frame counter during restore --- zigpy_cli/radio.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 5f54c04..952af5c 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -105,7 +105,9 @@ async def backup(app, output): @click_coroutine async def restore(app, input): obj = json.load(input) + network_info, node_info = zigpy.state.json_to_network_state(obj) + network_info.network_key_counter += 5000 await app.connect() await app.write_network_info(network_info=network_info, node_info=node_info) From b7478f9b4b6473f376bd9ebe225fc571e33b26ca Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 12 Dec 2021 17:55:26 -0500 Subject: [PATCH 04/16] Use old frame counter attribute --- zigpy_cli/radio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 952af5c..688555f 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -107,7 +107,7 @@ async def restore(app, input): obj = json.load(input) network_info, node_info = zigpy.state.json_to_network_state(obj) - network_info.network_key_counter += 5000 + network_info.network_key.tx_counter += 5000 await app.connect() await app.write_network_info(network_info=network_info, node_info=node_info) From a2792bcc12045f3890b093b25820282d354db8f2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:34:40 -0400 Subject: [PATCH 05/16] Make the baudrate configurable --- zigpy_cli/radio.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 688555f..ccbf3fe 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -6,7 +6,6 @@ import click import zigpy.state -import zigpy.config as conf from zigpy_cli.cli import cli, click_coroutine from zigpy_cli.common import RADIO_TO_PYPI, RADIO_TO_PACKAGE, RADIO_LOGGING_CONFIGS @@ -18,8 +17,9 @@ @click.pass_context @click.argument("radio", type=click.Choice(list(RADIO_TO_PACKAGE.keys()))) @click.argument("port", type=str) +@click.option("--baudrate", type=int, default=None) @click_coroutine -async def radio(ctx, radio, port): +async def radio(ctx, radio, port, baudrate=None): # Setup logging for the radio verbose = ctx.parent.params["verbose"] logging_configs = RADIO_LOGGING_CONFIGS[radio] @@ -41,13 +41,11 @@ async def radio(ctx, radio, port): # Start the radio app_cls = radio_module.ControllerApplication - config = app_cls.SCHEMA( - { - conf.CONF_DEVICE: { - conf.CONF_DEVICE_PATH: port, - }, - } - ) + config = app_cls.SCHEMA({"device": {"path": port}}) + + if baudrate is not None: + config["device"]["baudrate"] = baudrate + app = app_cls(config) ctx.obj = app From 8f244476224c6644de58145742d320dde6c281a5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:36:37 -0400 Subject: [PATCH 06/16] Add an energy scan tool --- zigpy_cli/radio.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index ccbf3fe..ae8f6d5 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -3,9 +3,12 @@ import json import logging import importlib +import collections import click import zigpy.state +import zigpy.types +import zigpy.zdo.types from zigpy_cli.cli import cli, click_coroutine from zigpy_cli.common import RADIO_TO_PYPI, RADIO_TO_PACKAGE, RADIO_LOGGING_CONFIGS @@ -118,3 +121,51 @@ async def form(app): await app.connect() await app.startup(auto_form=True) await app.form_network() + + +@radio.command() +@click.pass_obj +@click_coroutine +async def energy_scan(app): + await app.startup() + LOGGER.info("Running scan...") + + # We compute an average over the last 5 scans + channel_energies = collections.defaultdict(lambda: collections.deque([], maxlen=5)) + + while True: + rsp = await app.get_device(nwk=0x0000).zdo.Mgmt_NWK_Update_req( + zigpy.zdo.types.NwkUpdate( + ScanChannels=zigpy.types.Channels.ALL_CHANNELS, + ScanDuration=0x02, + ScanCount=1, + ) + ) + + _, scanned_channels, _, _, energy_values = rsp + + for channel, energy in zip(scanned_channels, energy_values): + energies = channel_energies[channel] + energies.append(energy) + + total = 0xFF * len(energies) + + print(f"Channel energy (mean of {len(energies)} / {energies.maxlen}):") + print("------------------------------------------------") + print(" + Lower energy is better") + print(" + Active Zigbee networks on a channel may still cause congestion") + print(" + TX on 26 in North America may be with lower power due to regulations") + print(" + Zigbee channels 15, 20, 25 fall between WiFi channels 1, 6, 11") + print(" + Some Zigbee devices only join networks on channels 15, 20, and 25") + print("------------------------------------------------") + + for channel, energies in channel_energies.items(): + count = sum(energies) + asterisk = "*" if channel == 26 else " " + + print( + f" - {channel:>02}{asterisk} {count / total:>7.2%} " + + "#" * int(100 * count / total) + ) + + print() From 45f1751bf66ead5ae6811b7eaa7a6037d7627954 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 10 Apr 2022 21:04:21 -0400 Subject: [PATCH 07/16] Upgrade pre-commit deps --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eec6500..00faec0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black args: - --safe - --quiet - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 4.0.1 hooks: - id: flake8 - repo: https://github.com/PyCQA/isort - rev: 5.5.2 + rev: 5.10.1 hooks: - id: isort From 7f26869d895d7bde174f030e2d2d98d14a5ef8c6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 10 Apr 2022 21:04:46 -0400 Subject: [PATCH 08/16] Allow energy scans to be performed using non-coordinator devices --- zigpy_cli/common.py | 21 +++++++++++++++++++++ zigpy_cli/radio.py | 19 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/zigpy_cli/common.py b/zigpy_cli/common.py index 1042fc6..ad2e512 100644 --- a/zigpy_cli/common.py +++ b/zigpy_cli/common.py @@ -1,5 +1,7 @@ import logging +import click + TRACE = logging.DEBUG - 5 logging.addLevelName(TRACE, "TRACE") @@ -69,3 +71,22 @@ } RADIO_TO_PYPI = {name: mod.replace("_", "-") for name, mod in RADIO_TO_PACKAGE.items()} + + +class HexOrDecIntParamType(click.ParamType): + name = "integer" + + def convert(self, value, param, ctx): + if isinstance(value, int): + return value + + try: + if value[:2].lower() == "0x": + return int(value[2:], 16) + else: + return int(value, 10) + except ValueError: + self.fail(f"{value!r} is not a valid integer", param, ctx) + + +HEX_OR_DEC_INT = HexOrDecIntParamType() diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index ae8f6d5..841c904 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -11,7 +11,12 @@ import zigpy.zdo.types from zigpy_cli.cli import cli, click_coroutine -from zigpy_cli.common import RADIO_TO_PYPI, RADIO_TO_PACKAGE, RADIO_LOGGING_CONFIGS +from zigpy_cli.common import ( + RADIO_TO_PYPI, + HEX_OR_DEC_INT, + RADIO_TO_PACKAGE, + RADIO_LOGGING_CONFIGS, +) LOGGER = logging.getLogger(__name__) @@ -125,16 +130,24 @@ async def form(app): @radio.command() @click.pass_obj +@click.option("--nwk", type=HEX_OR_DEC_INT, default=0x0000) @click_coroutine -async def energy_scan(app): +async def energy_scan(app, nwk): await app.startup() LOGGER.info("Running scan...") + # Temporarily create a zigpy device for scans not using the coordinator itself + if nwk != 0x0000: + app.add_device( + nwk=nwk, + ieee=zigpy.types.EUI64.convert("AA:AA:AA:AA:AA:AA:AA:AA"), + ) + # We compute an average over the last 5 scans channel_energies = collections.defaultdict(lambda: collections.deque([], maxlen=5)) while True: - rsp = await app.get_device(nwk=0x0000).zdo.Mgmt_NWK_Update_req( + rsp = await app.get_device(nwk=nwk).zdo.Mgmt_NWK_Update_req( zigpy.zdo.types.NwkUpdate( ScanChannels=zigpy.types.Channels.ALL_CHANNELS, ScanDuration=0x02, From fbfc8bc1a0e07ece74f6a98f03acb8b50952e3c4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 May 2022 12:22:23 -0400 Subject: [PATCH 09/16] Explicitly handle missing modules --- zigpy_cli/radio.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 841c904..f428b38 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -36,17 +36,18 @@ async def radio(ctx, radio, port, baudrate=None): for logger, level in logging_config.items(): logging.getLogger(logger).setLevel(level) - # Import the radio library module = RADIO_TO_PACKAGE[radio] + ".zigbee.application" - try: - radio_module = importlib.import_module(module) - except ImportError: + # Catching just `ImportError` masks dependency errors and is annoying + if importlib.util.find_spec(module) is None: raise click.ClickException( f"Radio module for {radio!r} is not installed." f" Install it with `pip install {RADIO_TO_PYPI[radio]}`." ) + # Import the radio library + radio_module = importlib.import_module(module) + # Start the radio app_cls = radio_module.ControllerApplication config = app_cls.SCHEMA({"device": {"path": port}}) From a96fda43608e8de4bb9765b17e65cda270c1be66 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 May 2022 12:22:42 -0400 Subject: [PATCH 10/16] Do not unnecessarily call `connect` --- zigpy_cli/radio.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index f428b38..3a54e88 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -124,9 +124,7 @@ async def restore(app, input): @click.pass_obj @click_coroutine async def form(app): - await app.connect() await app.startup(auto_form=True) - await app.form_network() @radio.command() From a5c2d4fa0ad577967bb73cbab7276a6dfae1ea28 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 14 May 2022 19:49:47 -0400 Subject: [PATCH 11/16] Remove `source`, since radio libraries will now provide it --- zigpy_cli/radio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index c114a85..2878348 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -100,7 +100,6 @@ async def backup(app, output): obj = zigpy.state.network_state_to_json( network_info=app.state.network_info, node_info=app.state.node_info, - source="zigpy-cli@0.0.1", ) output.write(json.dumps(obj, indent=4)) From 3e6503eec620a141772b6d22dacaea1b900bfc7c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 21 May 2022 22:38:30 -0400 Subject: [PATCH 12/16] Remove `click` from `setup.py` runtime dependencies --- setup.py | 4 +-- zigpy_cli/cli.py | 2 +- zigpy_cli/common.py | 72 --------------------------------------------- zigpy_cli/const.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ zigpy_cli/radio.py | 8 ++--- 5 files changed, 76 insertions(+), 81 deletions(-) create mode 100644 zigpy_cli/const.py diff --git a/setup.py b/setup.py index 7414d33..9f39e6b 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages import zigpy_cli -import zigpy_cli.common +import zigpy_cli.const setup( name="zigpy-cli", @@ -25,7 +25,7 @@ ], extras_require={ # [all] pulls in all radio libraries - "all": zigpy_cli.common.RADIO_TO_PYPI.values(), + "all": zigpy_cli.const.RADIO_TO_PYPI.values(), "testing": [ "pytest>=5.4.5", "pytest-asyncio>=0.12.0", diff --git a/zigpy_cli/cli.py b/zigpy_cli/cli.py index 9029e82..5cd10df 100644 --- a/zigpy_cli/cli.py +++ b/zigpy_cli/cli.py @@ -7,7 +7,7 @@ import click import coloredlogs -from zigpy_cli.common import LOG_LEVELS +from zigpy_cli.const import LOG_LEVELS LOGGER = logging.getLogger(__name__) diff --git a/zigpy_cli/common.py b/zigpy_cli/common.py index ad2e512..9cd9fc3 100644 --- a/zigpy_cli/common.py +++ b/zigpy_cli/common.py @@ -1,77 +1,5 @@ -import logging - import click -TRACE = logging.DEBUG - 5 -logging.addLevelName(TRACE, "TRACE") - - -LOG_LEVELS = [logging.WARNING, logging.INFO, logging.DEBUG, TRACE] - - -RADIO_TO_PACKAGE = { - "ezsp": "bellows", - "deconz": "zigpy_deconz", - "xbee": "zigpy_xbee", - "zigate": "zigpy_zigate", - "znp": "zigpy_znp", -} - - -RADIO_LOGGING_CONFIGS = { - "ezsp": [ - { - "bellows.zigbee.application": logging.INFO, - "bellows.ezsp": logging.INFO, - }, - { - "bellows.zigbee.application": logging.DEBUG, - "bellows.ezsp": logging.DEBUG, - }, - ], - "deconz": [ - { - "zigpy_deconz.zigbee.application": logging.INFO, - "zigpy_deconz.api": logging.INFO, - }, - { - "zigpy_deconz.zigbee.application": logging.DEBUG, - "zigpy_deconz.api": logging.DEBUG, - }, - ], - "xbee": [ - { - "zigpy_xbee.zigbee.application": logging.INFO, - "zigpy_xbee.api": logging.INFO, - }, - { - "zigpy_xbee.zigbee.application": logging.DEBUG, - "zigpy_xbee.api": logging.DEBUG, - }, - ], - "zigate": [ - { - "zigpy_zigate": logging.INFO, - }, - { - "zigpy_zigate": logging.DEBUG, - }, - ], - "znp": [ - { - "zigpy_znp": logging.INFO, - }, - { - "zigpy_znp": logging.DEBUG, - }, - { - "zigpy_znp": TRACE, - }, - ], -} - -RADIO_TO_PYPI = {name: mod.replace("_", "-") for name, mod in RADIO_TO_PACKAGE.items()} - class HexOrDecIntParamType(click.ParamType): name = "integer" diff --git a/zigpy_cli/const.py b/zigpy_cli/const.py new file mode 100644 index 0000000..1042fc6 --- /dev/null +++ b/zigpy_cli/const.py @@ -0,0 +1,71 @@ +import logging + +TRACE = logging.DEBUG - 5 +logging.addLevelName(TRACE, "TRACE") + + +LOG_LEVELS = [logging.WARNING, logging.INFO, logging.DEBUG, TRACE] + + +RADIO_TO_PACKAGE = { + "ezsp": "bellows", + "deconz": "zigpy_deconz", + "xbee": "zigpy_xbee", + "zigate": "zigpy_zigate", + "znp": "zigpy_znp", +} + + +RADIO_LOGGING_CONFIGS = { + "ezsp": [ + { + "bellows.zigbee.application": logging.INFO, + "bellows.ezsp": logging.INFO, + }, + { + "bellows.zigbee.application": logging.DEBUG, + "bellows.ezsp": logging.DEBUG, + }, + ], + "deconz": [ + { + "zigpy_deconz.zigbee.application": logging.INFO, + "zigpy_deconz.api": logging.INFO, + }, + { + "zigpy_deconz.zigbee.application": logging.DEBUG, + "zigpy_deconz.api": logging.DEBUG, + }, + ], + "xbee": [ + { + "zigpy_xbee.zigbee.application": logging.INFO, + "zigpy_xbee.api": logging.INFO, + }, + { + "zigpy_xbee.zigbee.application": logging.DEBUG, + "zigpy_xbee.api": logging.DEBUG, + }, + ], + "zigate": [ + { + "zigpy_zigate": logging.INFO, + }, + { + "zigpy_zigate": logging.DEBUG, + }, + ], + "znp": [ + { + "zigpy_znp": logging.INFO, + }, + { + "zigpy_znp": logging.DEBUG, + }, + { + "zigpy_znp": TRACE, + }, + ], +} + +RADIO_TO_PYPI = {name: mod.replace("_", "-") for name, mod in RADIO_TO_PACKAGE.items()} diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 2878348..8e49667 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -11,12 +11,8 @@ import zigpy.zdo.types from zigpy_cli.cli import cli, click_coroutine -from zigpy_cli.common import ( - RADIO_TO_PYPI, - HEX_OR_DEC_INT, - RADIO_TO_PACKAGE, - RADIO_LOGGING_CONFIGS, -) +from zigpy_cli.const import RADIO_TO_PYPI, RADIO_TO_PACKAGE, RADIO_LOGGING_CONFIGS +from zigpy_cli.common import HEX_OR_DEC_INT LOGGER = logging.getLogger(__name__) From 759e918ac91b7cf5dfb532802dd0fc32376c61f9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 21 May 2022 22:50:49 -0400 Subject: [PATCH 13/16] Explicitly import `importlib.util` --- zigpy_cli/radio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 8e49667..67dc34a 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -4,6 +4,7 @@ import logging import importlib import collections +import importlib.util import click import zigpy.state From f962747c477a60cb146a0aa53f123bedde32c3a3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 27 Jun 2022 20:37:57 -0400 Subject: [PATCH 14/16] Always install the common radio libraries --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9f39e6b..0c85c17 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,6 @@ from setuptools import setup, find_packages import zigpy_cli -import zigpy_cli.const setup( name="zigpy-cli", @@ -18,14 +17,16 @@ entry_points={"console_scripts": ["zigpy=zigpy_cli.__main__:cli"]}, packages=find_packages(exclude=["tests", "tests.*"]), install_requires=[ - "zigpy", "click", "coloredlogs", "scapy", + "zigpy>=0.47.1", + "bellows>=0.31.0", + "zigpy-deconz>=0.18.0", + "zigpy-znp>=0.8.0", ], extras_require={ # [all] pulls in all radio libraries - "all": zigpy_cli.const.RADIO_TO_PYPI.values(), "testing": [ "pytest>=5.4.5", "pytest-asyncio>=0.12.0", From 89c18feee8c1417d79a56a3d4197fd22bd1886b7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 27 Jun 2022 20:46:16 -0400 Subject: [PATCH 15/16] Provide a way to increment the frame counter during restore --- zigpy_cli/radio.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 67dc34a..d2126f7 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -104,13 +104,14 @@ async def backup(app, output): @radio.command() @click.argument("input", type=click.File("r")) +@click.option("-c", "--frame-counter-increment", type=int, default=5000) @click.pass_obj @click_coroutine -async def restore(app, input): +async def restore(app, frame_counter_increment, input): obj = json.load(input) network_info, node_info = zigpy.state.json_to_network_state(obj) - network_info.network_key.tx_counter += 5000 + network_info.network_key.tx_counter += frame_counter_increment await app.connect() await app.write_network_info(network_info=network_info, node_info=node_info) From 645bba04413db1ae80480bef1b48414959a26028 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 27 Jun 2022 20:46:53 -0400 Subject: [PATCH 16/16] Indicate the current channel in the energy scan --- zigpy_cli/radio.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index d2126f7..fe676df 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -167,14 +167,23 @@ async def energy_scan(app, nwk): print(" + TX on 26 in North America may be with lower power due to regulations") print(" + Zigbee channels 15, 20, 25 fall between WiFi channels 1, 6, 11") print(" + Some Zigbee devices only join networks on channels 15, 20, and 25") + print(" + Current channel is enclosed in [square brackets]") print("------------------------------------------------") for channel, energies in channel_energies.items(): count = sum(energies) asterisk = "*" if channel == 26 else " " + if channel == app.state.network_info.channel: + bracket_open = "[" + bracket_close = "]" + else: + bracket_open = " " + bracket_close = " " + print( - f" - {channel:>02}{asterisk} {count / total:>7.2%} " + f" - {bracket_open}{channel:>02}{asterisk}{bracket_close}" + + f" {count / total:>7.2%} " + "#" * int(100 * count / total) )