Skip to content

Commit e919f80

Browse files
authored
Merge pull request #11 from puddly/puddly/energy-scans
Implement an `energy-scan` tool
2 parents 5f6c2af + db811dd commit e919f80

File tree

4 files changed

+120
-4
lines changed

4 files changed

+120
-4
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
repos:
22
- repo: https://github.com/psf/black
3-
rev: 20.8b1
3+
rev: 22.3.0
44
hooks:
55
- id: black
66
args:
77
- --safe
88
- --quiet
99
- repo: https://gitlab.com/pycqa/flake8
10-
rev: 3.8.3
10+
rev: 4.0.1
1111
hooks:
1212
- id: flake8
1313
- repo: https://github.com/PyCQA/isort
14-
rev: 5.5.2
14+
rev: 5.10.1
1515
hooks:
1616
- id: isort

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,36 @@ Network key sequence: 0
6767
2021-07-12 13:25:15.316 host zigpy_znp.uart DEBUG Closing serial port
6868
```
6969

70+
Performing an energy scan (using a router device instead of the coordinator):
71+
72+
```console
73+
$ zigpy radio znp /dev/cu.usbserial-1420 energy-scan --nwk 0xbc6a
74+
75+
Channel energy (mean of 1 / 5):
76+
------------------------------------------------
77+
+ Lower energy is better
78+
+ Active Zigbee networks on a channel may still cause congestion
79+
+ TX on 26 in North America may be with lower power due to regulations
80+
+ Zigbee channels 15, 20, 25 fall between WiFi channels 1, 6, 11
81+
+ Some Zigbee devices only join networks on channels 15, 20, and 25
82+
------------------------------------------------
83+
- 11 80.00% ################################################################################
84+
- 12 83.53% ###################################################################################
85+
- 13 83.14% ###################################################################################
86+
- 14 78.82% ##############################################################################
87+
- 15 76.47% ############################################################################
88+
- 16 72.16% ########################################################################
89+
- 17 76.47% ############################################################################
90+
- 18 75.69% ###########################################################################
91+
- 19 72.16% ########################################################################
92+
- 20 65.49% #################################################################
93+
- 21 66.67% ##################################################################
94+
- 22 70.59% ######################################################################
95+
- 23 80.00% ################################################################################
96+
- 24 64.31% ################################################################
97+
- 25 77.25% #############################################################################
98+
- 26* 81.96% #################################################################################
99+
```
70100

71101
# OTA
72102
Display basic information about OTA files:

zigpy_cli/common.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logging
22

3+
import click
4+
35
TRACE = logging.DEBUG - 5
46
logging.addLevelName(TRACE, "TRACE")
57

@@ -69,3 +71,22 @@
6971
}
7072

7173
RADIO_TO_PYPI = {name: mod.replace("_", "-") for name, mod in RADIO_TO_PACKAGE.items()}
74+
75+
76+
class HexOrDecIntParamType(click.ParamType):
77+
name = "integer"
78+
79+
def convert(self, value, param, ctx):
80+
if isinstance(value, int):
81+
return value
82+
83+
try:
84+
if value[:2].lower() == "0x":
85+
return int(value[2:], 16)
86+
else:
87+
return int(value, 10)
88+
except ValueError:
89+
self.fail(f"{value!r} is not a valid integer", param, ctx)
90+
91+
92+
HEX_OR_DEC_INT = HexOrDecIntParamType()

zigpy_cli/radio.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22

33
import logging
44
import importlib
5+
import collections
56

67
import click
8+
import zigpy.state
9+
import zigpy.types
710
import zigpy.config as conf
11+
import zigpy.zdo.types
812

913
from zigpy_cli.cli import cli, click_coroutine
1014
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+
)
1221

1322
LOGGER = logging.getLogger(__name__)
1423

@@ -96,3 +105,59 @@ async def form(app):
96105
await app.startup(auto_form=True)
97106
await app.form_network()
98107
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

Comments
 (0)