From 1de3134b3bbf798b09f0b63d58270c997fc17760 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:03:54 -0400 Subject: [PATCH 1/5] WIP: Advanced energy scan --- zigpy_cli/radio.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 089069f..b7f1540 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -7,6 +7,8 @@ import itertools import json import logging +import random +import time import click import zigpy.state @@ -227,6 +229,57 @@ async def energy_scan(app, num_scans): print() +@radio.command() +@click.pass_obj +@click.option("-n", "--num-scans", type=int, default=10 * 2**8) +@click.option("-r", "--randomize", type=bool, default=True) +@click.argument("output", type=click.File("w"), default="-") +@click_coroutine +async def advanced_energy_scan(app, output, num_scans, randomize): + await app.startup() + LOGGER.info("Running scan...") + + channels = zigpy.types.Channels.ALL_CHANNELS + scan_counts = {channel: num_scans for channel in channels} + + if randomize: + + def iter_channels(): + while scan_counts: + channel = random.choice(tuple(scan_counts)) + scan_counts[channel] -= 1 + + yield channel + + if scan_counts[channel] <= 0: + del scan_counts[channel] + + else: + + def iter_channels(): + for channel, count in scan_counts.items(): + for i in range(count): + yield channel + + with click.progressbar( + iterable=iter_channels(), + length=len(list(channels)) * num_scans, + item_show_func=lambda item: None if item is None else f"Channel {item}", + ) as bar: + output.write("Timestamp,Channel,Energy\n") + + for channel in bar: + results = await app.energy_scan( + channels=zigpy.types.Channels.from_channel_list([channel]), + duration_exp=0, + count=1, + ) + + energy = results[channel] + timestamp = time.time() + output.write(f"{timestamp:0.4f},{channel},{energy:0.4f}\n") + + @radio.command() @click.pass_obj @click.option("-c", "--channel", type=int) From d8bc230fedc05a643af5e19035fa747cddc1a375 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:12:56 -0400 Subject: [PATCH 2/5] List nearby networks as well --- zigpy_cli/radio.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index b7f1540..2bf38c2 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -279,6 +279,25 @@ def iter_channels(): timestamp = time.time() output.write(f"{timestamp:0.4f},{channel},{energy:0.4f}\n") + import bellows.types + from bellows.zigbee.application import ( + ControllerApplication as EzspControllerApplication, + ) + + if not isinstance(app, EzspControllerApplication): + return + + await asyncio.sleep(1) + for channel in channels: + networks = await app._ezsp.startScan( + scanType=bellows.types.EzspNetworkScanType.ACTIVE_SCAN, + channelMask=zigpy.types.Channels.from_channel_list([channel]), + duration=6, + ) + + for network, lqi, rssi in networks: + print(f"Found network {network}: LQI={lqi}, RSSI={rssi}") + @radio.command() @click.pass_obj From f775fb5c7da6a4bc194f264777fcd1852e698906 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 11 Aug 2024 21:39:23 -0400 Subject: [PATCH 3/5] Switch to JSON and dump network information --- zigpy_cli/radio.py | 87 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 6de4990..acd3005 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -228,16 +228,34 @@ async def energy_scan(app, num_scans): @radio.command() @click.pass_obj -@click.option("-n", "--num-scans", type=int, default=10 * 2**8) +@click.option("-e", "--num-energy-scans", type=int, default=10 * 2**8) +@click.option("-n", "--num-network-scans", type=int, default=5) @click.option("-r", "--randomize", type=bool, default=True) @click.argument("output", type=click.File("w"), default="-") @click_coroutine -async def advanced_energy_scan(app, output, num_scans, randomize): +async def advanced_energy_scan( + app, + output, + num_energy_scans, + num_network_scans, + randomize, +): + import bellows.types + from bellows.zigbee.application import ( + ControllerApplication as EzspControllerApplication, + ) + from bellows.zigbee.util import map_energy_to_rssi as ezsp_map_energy_to_rssi + await app.startup() LOGGER.info("Running scan...") channels = zigpy.types.Channels.ALL_CHANNELS - scan_counts = {channel: num_scans for channel in channels} + scan_counts = {channel: num_energy_scans for channel in channels} + + scan_data = { + "energy_scan": [], + "network_scan": [], + } if randomize: @@ -260,11 +278,9 @@ def iter_channels(): with click.progressbar( iterable=iter_channels(), - length=len(list(channels)) * num_scans, + length=len(list(channels)) * num_energy_scans, item_show_func=lambda item: None if item is None else f"Channel {item}", ) as bar: - output.write("Timestamp,Channel,Energy\n") - for channel in bar: results = await app.energy_scan( channels=zigpy.types.Channels.from_channel_list([channel]), @@ -272,28 +288,53 @@ def iter_channels(): count=1, ) - energy = results[channel] - timestamp = time.time() - output.write(f"{timestamp:0.4f},{channel},{energy:0.4f}\n") + rssi = None - import bellows.types - from bellows.zigbee.application import ( - ControllerApplication as EzspControllerApplication, - ) + if isinstance(app, EzspControllerApplication): + rssi = ezsp_map_energy_to_rssi(results[channel]) - if not isinstance(app, EzspControllerApplication): - return + scan_data["energy_scan"].append( + { + "timestamp": time.time(), + "channel": channel, + "energy": results[channel], + "rssi": rssi, + } + ) await asyncio.sleep(1) for channel in channels: - networks = await app._ezsp.startScan( - scanType=bellows.types.EzspNetworkScanType.ACTIVE_SCAN, - channelMask=zigpy.types.Channels.from_channel_list([channel]), - duration=6, - ) - - for network, lqi, rssi in networks: - print(f"Found network {network}: LQI={lqi}, RSSI={rssi}") + print(f"Scanning for networks on channel {channel}") + networks = set() + + for attempt in range(num_network_scans): + networks_scan = await app._ezsp.startScan( + scanType=bellows.types.EzspNetworkScanType.ACTIVE_SCAN, + channelMask=zigpy.types.Channels.from_channel_list([channel]), + duration=6, + ) + networks_scan = tuple( + [(network.freeze(), lqi, rssi) for network, lqi, rssi in networks_scan] + ) + new_networks = set(networks_scan) - networks + + for network, lqi, rssi in new_networks: + print(f"Found network {network}: LQI={lqi}, RSSI={rssi}") + scan_data["network_scan"].append( + { + "channel": channel, + "lqi": lqi, + "rssi": rssi, + "network": { + **network.as_dict(), + "extendedPanId": str(network.extendedPanId), + }, + } + ) + + networks.update(new_networks) + + json.dump(scan_data, output, separators=(",", ":")) @radio.command() From aa58eaa9997b79164d1b7c51ca40de29e0eccd09 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:37:50 -0400 Subject: [PATCH 4/5] Properly deduplicate scanned networks --- zigpy_cli/radio.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index acd3005..85af5e8 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -304,21 +304,25 @@ def iter_channels(): await asyncio.sleep(1) for channel in channels: - print(f"Scanning for networks on channel {channel}") networks = set() for attempt in range(num_network_scans): + print( + "Scanning for networks on channel" + f" {channel} ({attempt + 1} / {num_network_scans})" + ) networks_scan = await app._ezsp.startScan( scanType=bellows.types.EzspNetworkScanType.ACTIVE_SCAN, channelMask=zigpy.types.Channels.from_channel_list([channel]), duration=6, ) - networks_scan = tuple( - [(network.freeze(), lqi, rssi) for network, lqi, rssi in networks_scan] - ) - new_networks = set(networks_scan) - networks - for network, lqi, rssi in new_networks: + for network, lqi, rssi in networks_scan: + if network.replace(allowingJoin=None).freeze() in networks: + continue + + networks.add(network.replace(allowingJoin=None).freeze()) + print(f"Found network {network}: LQI={lqi}, RSSI={rssi}") scan_data["network_scan"].append( { @@ -332,8 +336,6 @@ def iter_channels(): } ) - networks.update(new_networks) - json.dump(scan_data, output, separators=(",", ":")) From ebc953d56f3ca576bfc47254356b0dcee2de4d2b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:41:15 -0400 Subject: [PATCH 5/5] Add the current channel as well --- zigpy_cli/radio.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/zigpy_cli/radio.py b/zigpy_cli/radio.py index 85af5e8..81fce2a 100644 --- a/zigpy_cli/radio.py +++ b/zigpy_cli/radio.py @@ -253,6 +253,7 @@ async def advanced_energy_scan( scan_counts = {channel: num_energy_scans for channel in channels} scan_data = { + "current_channel": app.state.network_info.channel, "energy_scan": [], "network_scan": [], } @@ -302,7 +303,10 @@ def iter_channels(): } ) - await asyncio.sleep(1) + if not isinstance(app, EzspControllerApplication): + json.dump(scan_data, output, separators=(",", ":")) + return + for channel in channels: networks = set() @@ -329,10 +333,11 @@ def iter_channels(): "channel": channel, "lqi": lqi, "rssi": rssi, - "network": { - **network.as_dict(), - "extendedPanId": str(network.extendedPanId), - }, + "allowing_join": network.allowingJoin, + "extended_pan_id": str(network.extendedPanId), + "nwk_update_id": network.nwkUpdateId, + "pan_id": network.panId, + "stack_profile": network.stackProfile, } )