|
2 | 2 |
|
3 | 3 | import logging
|
4 | 4 | import importlib
|
| 5 | +import collections |
5 | 6 |
|
6 | 7 | import click
|
| 8 | +import zigpy.state |
| 9 | +import zigpy.types |
7 | 10 | import zigpy.config as conf
|
| 11 | +import zigpy.zdo.types |
8 | 12 |
|
9 | 13 | from zigpy_cli.cli import cli, click_coroutine
|
10 | 14 | from zigpy_cli.utils import format_bytes
|
11 |
| -from zigpy_cli.common import RADIO_TO_PYPI, RADIO_TO_PACKAGE, RADIO_LOGGING_CONFIGS |
| 15 | +from zigpy_cli.common import ( |
| 16 | + RADIO_TO_PYPI, |
| 17 | + HEX_OR_DEC_INT, |
| 18 | + RADIO_TO_PACKAGE, |
| 19 | + RADIO_LOGGING_CONFIGS, |
| 20 | +) |
12 | 21 |
|
13 | 22 | LOGGER = logging.getLogger(__name__)
|
14 | 23 |
|
@@ -96,3 +105,59 @@ async def form(app):
|
96 | 105 | await app.startup(auto_form=True)
|
97 | 106 | await app.form_network()
|
98 | 107 | dump_app_info(app)
|
| 108 | + |
| 109 | + |
| 110 | +@radio.command() |
| 111 | +@click.pass_obj |
| 112 | +@click.option("--nwk", type=HEX_OR_DEC_INT, default=0x0000) |
| 113 | +@click_coroutine |
| 114 | +async def energy_scan(app, nwk): |
| 115 | + await app.startup() |
| 116 | + LOGGER.info("Running scan...") |
| 117 | + |
| 118 | + # Temporarily create a zigpy device for scans not using the coordinator itself |
| 119 | + if nwk != 0x0000: |
| 120 | + app.add_device( |
| 121 | + nwk=nwk, |
| 122 | + ieee=zigpy.types.EUI64.convert("AA:AA:AA:AA:AA:AA:AA:AA"), |
| 123 | + ) |
| 124 | + |
| 125 | + # We compute an average over the last 5 scans |
| 126 | + channel_energies = collections.defaultdict(lambda: collections.deque([], maxlen=5)) |
| 127 | + |
| 128 | + while True: |
| 129 | + rsp = await app.get_device(nwk=nwk).zdo.Mgmt_NWK_Update_req( |
| 130 | + zigpy.zdo.types.NwkUpdate( |
| 131 | + ScanChannels=zigpy.types.Channels.ALL_CHANNELS, |
| 132 | + ScanDuration=0x02, |
| 133 | + ScanCount=1, |
| 134 | + ) |
| 135 | + ) |
| 136 | + |
| 137 | + _, scanned_channels, _, _, energy_values = rsp |
| 138 | + |
| 139 | + for channel, energy in zip(scanned_channels, energy_values): |
| 140 | + energies = channel_energies[channel] |
| 141 | + energies.append(energy) |
| 142 | + |
| 143 | + total = 0xFF * len(energies) |
| 144 | + |
| 145 | + print(f"Channel energy (mean of {len(energies)} / {energies.maxlen}):") |
| 146 | + print("------------------------------------------------") |
| 147 | + print(" + Lower energy is better") |
| 148 | + print(" + Active Zigbee networks on a channel may still cause congestion") |
| 149 | + print(" + TX on 26 in North America may be with lower power due to regulations") |
| 150 | + print(" + Zigbee channels 15, 20, 25 fall between WiFi channels 1, 6, 11") |
| 151 | + print(" + Some Zigbee devices only join networks on channels 15, 20, and 25") |
| 152 | + print("------------------------------------------------") |
| 153 | + |
| 154 | + for channel, energies in channel_energies.items(): |
| 155 | + count = sum(energies) |
| 156 | + asterisk = "*" if channel == 26 else " " |
| 157 | + |
| 158 | + print( |
| 159 | + f" - {channel:>02}{asterisk} {count / total:>7.2%} " |
| 160 | + + "#" * int(100 * count / total) |
| 161 | + ) |
| 162 | + |
| 163 | + print() |
0 commit comments