Skip to content

Commit f1eeca8

Browse files
authored
Channel energy scan (#149)
* XBee: energy scan * convert values * add test * improve test * apply suggestions from code review
1 parent f387a0f commit f1eeca8

File tree

3 files changed

+64
-2
lines changed

3 files changed

+64
-2
lines changed

tests/test_application.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,3 +597,32 @@ async def test_move_network_to_channel(app):
597597

598598
assert len(app._api._queued_at.mock_calls) == 1
599599
app._api._queued_at.assert_any_call("SC", 1 << (26 - 11))
600+
601+
602+
async def test_energy_scan(app):
603+
rssi = b"\x0A\x0F\x14\x19\x1E\x23\x28\x2D\x32\x37\x3C\x41\x46\x4B\x50\x55"
604+
app._api._at_command = mock.AsyncMock(spec=XBee._at_command, return_value=rssi)
605+
time_s = 3
606+
count = 3
607+
energy = await app.energy_scan(
608+
channels=[x for x in range(11, 27)], duration_exp=time_s, count=count
609+
)
610+
assert app._api._at_command.mock_calls == [mock.call("ED", time_s)] * count
611+
assert {k: round(v, 3) for k, v in energy.items()} == {
612+
11: 254.032,
613+
12: 253.153,
614+
13: 251.486,
615+
14: 248.352,
616+
15: 242.562,
617+
16: 232.193,
618+
17: 214.619,
619+
18: 187.443,
620+
19: 150.853,
621+
20: 109.797,
622+
21: 72.172,
623+
22: 43.571,
624+
23: 24.769,
625+
24: 13.56,
626+
25: 7.264,
627+
26: 3.844,
628+
}

zigpy_xbee/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
216216
"D7": t.uint8_t, # 0 - 7 (an Enum)
217217
"P3": t.uint8_t, # 0 - 5 (an Enum)
218218
"P4": t.uint8_t, # 0 - 5 (an Enum)
219+
# MAC diagnostics commands
220+
"ED": t.Bytes, # 16-byte value
219221
# I/O commands
220222
"IR": t.uint16_t,
221223
"IC": t.uint16_t,

zigpy_xbee/zigbee/application.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import asyncio
44
import logging
5+
import math
6+
import statistics
57
import time
68
from typing import Any
79

@@ -181,9 +183,38 @@ async def energy_scan(
181183
self, channels: zigpy.types.Channels, duration_exp: int, count: int
182184
) -> dict[int, float]:
183185
"""Runs an energy detection scan and returns the per-channel scan results."""
186+
all_results = {}
184187

185-
LOGGER.warning("Coordinator does not support energy scanning")
186-
return {c: 0 for c in channels}
188+
for _ in range(count):
189+
results = await self._api._at_command("ED", duration_exp)
190+
results = {
191+
channel: -int(rssi) for channel, rssi in zip(range(11, 27), results)
192+
}
193+
194+
for channel, rssi in results.items():
195+
all_results.setdefault(channel, []).append(rssi)
196+
197+
def logistic(x: float, *, L: float = 1, x_0: float = 0, k: float = 1) -> float:
198+
"""Logistic function."""
199+
return L / (1 + math.exp(-k * (x - x_0)))
200+
201+
def map_rssi_to_energy(rssi: int) -> float:
202+
"""Remaps RSSI (in dBm) to Energy (0-255)."""
203+
RSSI_MAX = -5
204+
RSSI_MIN = -92
205+
return logistic(
206+
x=rssi,
207+
L=255,
208+
x_0=RSSI_MIN + 0.45 * (RSSI_MAX - RSSI_MIN),
209+
k=0.13,
210+
)
211+
212+
energy = {
213+
channel: map_rssi_to_energy(statistics.mean(all_rssi))
214+
for channel, all_rssi in all_results.items()
215+
}
216+
217+
return {channel: energy.get(channel, 0) for channel in channels}
187218

188219
async def force_remove(self, dev):
189220
"""Forcibly remove device from NCP."""

0 commit comments

Comments
 (0)