Skip to content

Commit a076d7a

Browse files
committed
Properly handle older Z-Stack SYS.SetTxPower responses
1 parent 9bdf3ae commit a076d7a

File tree

4 files changed

+62
-18
lines changed

4 files changed

+62
-18
lines changed

tests/application/test_startup.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import zigpy_znp.config as conf
55
import zigpy_znp.commands as c
66
from zigpy_znp.api import ZNP
7+
from zigpy_znp.exceptions import InvalidCommandResponse
78
from zigpy_znp.types.nvids import ExNvIds, OsalNvIds
89

910
from ..conftest import (
@@ -163,19 +164,46 @@ async def test_write_nvram(device, make_application, mocker):
163164

164165

165166
@pytest.mark.parametrize("device", FORMED_DEVICES)
166-
async def test_tx_power(device, make_application):
167+
@pytest.mark.parametrize("succeed", [True, False])
168+
async def test_tx_power(device, succeed, make_application):
167169
app, znp_server = make_application(
168170
server_cls=device,
169171
client_config={conf.CONF_ZNP_CONFIG: {conf.CONF_TX_POWER: 19}},
170172
)
171173

172-
set_tx_power = znp_server.reply_once_to(
173-
request=c.SYS.SetTxPower.Req(TXPower=19),
174-
responses=[c.SYS.SetTxPower.Rsp(Status=t.Status.SUCCESS)],
175-
)
176-
177-
await app.startup(auto_form=False)
178-
await set_tx_power
174+
if device.version == 3.30:
175+
if succeed:
176+
set_tx_power = znp_server.reply_once_to(
177+
request=c.SYS.SetTxPower.Req(TXPower=19),
178+
responses=[c.SYS.SetTxPower.Rsp(StatusOrPower=t.Status.SUCCESS)],
179+
)
180+
else:
181+
set_tx_power = znp_server.reply_once_to(
182+
request=c.SYS.SetTxPower.Req(TXPower=19),
183+
responses=[
184+
c.SYS.SetTxPower.Rsp(StatusOrPower=t.Status.INVALID_PARAMETER)
185+
],
186+
)
187+
else:
188+
if succeed:
189+
set_tx_power = znp_server.reply_once_to(
190+
request=c.SYS.SetTxPower.Req(TXPower=19),
191+
responses=[c.SYS.SetTxPower.Rsp(StatusOrPower=19)],
192+
)
193+
else:
194+
set_tx_power = znp_server.reply_once_to(
195+
request=c.SYS.SetTxPower.Req(TXPower=19),
196+
responses=[c.SYS.SetTxPower.Rsp(StatusOrPower=-1)], # adjusted
197+
)
198+
199+
if device.version == 3.30 and not succeed:
200+
with pytest.raises(InvalidCommandResponse):
201+
await app.startup(auto_form=False)
202+
203+
await set_tx_power
204+
else:
205+
await app.startup(auto_form=False)
206+
await set_tx_power
179207

180208
await app.shutdown()
181209

zigpy_znp/commands/sys.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,10 +418,11 @@ class SYS(t.CommandsBase, subsystem=t.Subsystem.SYS):
418418
t.CommandType.SREQ,
419419
0x14,
420420
req_schema=(t.Param("TXPower", t.int8s, "Requested TX power setting, in dBm"),),
421-
# While the docs say "the returned TX power is the actual setting applied to
422-
# the radio - nearest characterized value for the specific radio.", the code
423-
# matches the documentation.
424-
rsp_schema=t.STATUS_SCHEMA,
421+
# XXX: Z-Stack 3.30+ returns SUCCESS or INVALID_PARAMETER.
422+
# Z-Stack 1.2 and 3.0 return the cloest TX power setting.
423+
rsp_schema=(
424+
t.Param("StatusOrPower", t.int8s, "Status code or applied power setting"),
425+
),
425426
)
426427

427428
# initialize the statistics table in NV memory

zigpy_znp/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def validator(v):
6363
vol.Optional(CONF_ZNP_CONFIG, default={}): vol.Schema(
6464
{
6565
vol.Optional(CONF_TX_POWER, default=None): vol.Any(
66-
None, vol.All(int, vol.Range(min=-22, max=19))
66+
None, vol.All(int, vol.Range(min=-22, max=22))
6767
),
6868
vol.Optional(CONF_SREQ_TIMEOUT, default=15): VolPositiveNumber,
6969
vol.Optional(CONF_ARSP_TIMEOUT, default=30): VolPositiveNumber,

zigpy_znp/zigbee/application.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,7 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False):
254254
# At this point the device state should the same, regardless of whether we just
255255
# formed a new network or are restoring one
256256
if self.znp_config[conf.CONF_TX_POWER] is not None:
257-
dbm = self.znp_config[conf.CONF_TX_POWER]
258-
259-
await self._znp.request(
260-
c.SYS.SetTxPower.Req(TXPower=dbm), RspStatus=t.Status.SUCCESS
261-
)
257+
await self.set_tx_power(dbm=self.znp_config[conf.CONF_TX_POWER])
262258

263259
# Both versions of Z-Stack use this callback
264260
started_as_coordinator = self._znp.wait_for_response(
@@ -388,6 +384,25 @@ async def update_pan_id(self, pan_id: t.uint16_t) -> None:
388384
await self._znp.nvram.osal_write(OsalNvIds.NIB, nib)
389385
await self._znp.nvram.osal_write(OsalNvIds.PANID, pan_id)
390386

387+
async def set_tx_power(self, dbm: int) -> None:
388+
"""
389+
Sets the radio TX power.
390+
"""
391+
392+
rsp = await self._znp.request(c.SYS.SetTxPower.Req(TXPower=dbm))
393+
394+
if self._znp.version >= 3.30 and rsp.StatusOrPower != t.Status.SUCCESS:
395+
# Z-Stack 3's response indicates success or failure
396+
raise InvalidCommandResponse(
397+
f"Failed to set TX power: {t.Status(rsp.StatusOrPower)!r}", rsp
398+
)
399+
elif self._znp.version < 3.30 and rsp.StatusOrPower != dbm:
400+
# Old Z-Stack releases used the response status field to indicate the power
401+
# setting that was actually applied
402+
LOGGER.warning(
403+
"Requested TX power %d was adjusted to %d", dbm, rsp.StatusOrPower
404+
)
405+
391406
async def form_network(self):
392407
"""
393408
Clears the current config and forms a new network with a random network key,

0 commit comments

Comments
 (0)