Skip to content

Commit

Permalink
Convert command types to dictionaries (#636)
Browse files Browse the repository at this point in the history
* Migrate v10 to dicts

* Migrate v11 to dicts

* Migrate v9 to dicts

* Migrate v8 to dicts

* Migrate v7

* Migrate v6

* Migrate v5

* WIP: V4

* Drop conversion to tuples

* Support both tuples and dicts, for testing

* Fix tests

* Finish up v4

* Remove tuple handling, it's all dicts now

* Allow sending with args and kwargs

* Migrate `application.py` to send with kwargs

* Fix unit tests

* Migrate v4 to kwargs

* Migrate v5

* Migrate v6

* Migrate v7

* Migrate v8

* Migrate v9

* Migrate v10

* Migrate v13

* Migrate v14

* Fix tests

* Migrate legacy EZSP internals

* Migrate repairs

* Log sent command kwargs
  • Loading branch information
puddly authored Jul 23, 2024
1 parent 64ba03d commit 8eb257d
Show file tree
Hide file tree
Showing 37 changed files with 2,469 additions and 1,405 deletions.
70 changes: 42 additions & 28 deletions bellows/ezsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ def _switch_protocol_version(self, version: int) -> None:

async def version(self):
ver, stack_type, stack_version = await self._command(
"version", self.ezsp_version
"version", desiredProtocolVersion=self.ezsp_version
)
if ver != self.ezsp_version:
self._switch_protocol_version(ver)
await self._command("version", ver)
await self._command("version", desiredProtocolVersion=ver)
LOGGER.debug(
"EZSP Stack Type: %s, Stack Version: %04x, Protocol version: %s",
stack_type,
Expand Down Expand Up @@ -205,17 +205,22 @@ def _get_command_priority(self, name: str) -> int:
"getValue": 999,
}.get(name, 0)

async def _command(self, name: str, *args: tuple[Any, ...]) -> Any:
async def _command(self, name: str, *args: Any, **kwargs: Any) -> Any:
if not self.is_ezsp_running:
LOGGER.debug(
"Couldn't send command %s(%s). EZSP is not running", name, args
"Couldn't send command %s(%s, %s). EZSP is not running",
name,
args,
kwargs,
)
raise EzspError("EZSP is not running")

async with self._send_sem(priority=self._get_command_priority(name)):
return await self._protocol.command(name, *args)
return await self._protocol.command(name, *args, **kwargs)

async def _list_command(self, name, item_frames, completion_frame, spos, *args):
async def _list_command(
self, name, item_frames, completion_frame, spos, *args, **kwargs
):
"""Run a command, returning result callbacks as a list"""
fut = asyncio.Future()
results = []
Expand All @@ -228,7 +233,7 @@ def cb(frame_name, response):

cbid = self.add_callback(cb)
try:
v = await self._command(name, *args)
v = await self._command(name, *args, **kwargs)
if t.sl_Status.from_ember_status(v[0]) != t.sl_Status.OK:
raise Exception(v)
v = await fut
Expand Down Expand Up @@ -304,7 +309,7 @@ def __getattr__(self, name: str) -> Callable:

async def formNetwork(self, parameters: t.EmberNetworkParameters) -> None:
with self.wait_for_stack_status(t.sl_Status.NETWORK_UP) as stack_status:
v = await self._command("formNetwork", parameters)
v = await self._command("formNetwork", parameters=parameters)

if t.sl_Status.from_ember_status(v[0]) != t.sl_Status.OK:
raise zigpy.exceptions.FormationFailure(f"Failure forming network: {v}")
Expand Down Expand Up @@ -341,7 +346,7 @@ async def get_board_info(
tokens = {}

for token in (t.EzspMfgTokenId.MFG_STRING, t.EzspMfgTokenId.MFG_BOARD_NAME):
(value,) = await self.getMfgToken(token)
(value,) = await self.getMfgToken(tokenId=token)
LOGGER.debug("Read %s token: %s", token.name, value)

# Tokens are fixed-length and initially filled with \xFF
Expand All @@ -357,7 +362,9 @@ async def get_board_info(

tokens[token] = result

(status, ver_info_bytes) = await self.getValue(t.EzspValueId.VALUE_VERSION_INFO)
(status, ver_info_bytes) = await self.getValue(
valueId=t.EzspValueId.VALUE_VERSION_INFO
)
version = None

if t.sl_Status.from_ember_status(status) == t.sl_Status.OK:
Expand All @@ -381,7 +388,7 @@ async def _get_nv3_restored_eui64_key(self) -> t.NV3KeyId | None:
t.NV3KeyId.NVM3KEY_STACK_RESTORED_EUI64, # RCP firmware
):
try:
rsp = await self.getTokenData(key, 0)
rsp = await self.getTokenData(token=key, index=0)
except (InvalidCommandError, AttributeError):
# Either the command doesn't exist in the EZSP version, or the command
# is not implemented in the firmware
Expand All @@ -397,7 +404,7 @@ async def _get_nv3_restored_eui64_key(self) -> t.NV3KeyId | None:

async def _get_mfg_custom_eui_64(self) -> t.EUI64 | None:
"""Get the custom EUI 64 manufacturing token, if it has a valid value."""
(data,) = await self.getMfgToken(t.EzspMfgTokenId.MFG_CUSTOM_EUI_64)
(data,) = await self.getMfgToken(tokenId=t.EzspMfgTokenId.MFG_CUSTOM_EUI_64)

# Manufacturing tokens do not exist in RCP firmware: all reads are empty
if not data:
Expand Down Expand Up @@ -455,14 +462,15 @@ async def write_custom_eui64(
if nv3_eui64_key is not None:
# Prefer NV3 storage over MFG_CUSTOM_EUI_64, as it can be rewritten
(status,) = await self.setTokenData(
nv3_eui64_key,
0,
t.LVBytes32(ieee.serialize()),
token=nv3_eui64_key,
index=0,
token_data=t.LVBytes32(ieee.serialize()),
)
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
elif mfg_custom_eui64 is None and burn_into_userdata:
(status,) = await self.setMfgToken(
t.EzspMfgTokenId.MFG_CUSTOM_EUI_64, ieee.serialize()
tokenId=t.EzspMfgTokenId.MFG_CUSTOM_EUI_64,
tokenData=ieee.serialize(),
)
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
elif mfg_custom_eui64 is None and not burn_into_userdata:
Expand Down Expand Up @@ -497,20 +505,20 @@ def handle_callback(self, *args):
async def set_source_routing(self) -> None:
"""Enable source routing on NCP."""
res = await self.setConcentrator(
True,
t.EmberConcentratorType.HIGH_RAM_CONCENTRATOR,
MTOR_MIN_INTERVAL,
MTOR_MAX_INTERVAL,
MTOR_ROUTE_ERROR_THRESHOLD,
MTOR_DELIVERY_FAIL_THRESHOLD,
0,
on=True,
concentratorType=t.EmberConcentratorType.HIGH_RAM_CONCENTRATOR,
minTime=MTOR_MIN_INTERVAL,
maxTime=MTOR_MAX_INTERVAL,
routeErrorThreshold=MTOR_ROUTE_ERROR_THRESHOLD,
deliveryFailureThreshold=MTOR_DELIVERY_FAIL_THRESHOLD,
maxHops=0,
)
LOGGER.debug("Set concentrator type: %s", res)
if t.sl_Status.from_ember_status(res[0]) != t.sl_Status.OK:
LOGGER.warning("Couldn't set concentrator type %s: %s", True, res)

if self._ezsp_version >= 8:
await self.setSourceRouteDiscoveryMode(1)
await self.setSourceRouteDiscoveryMode(mode=1)

def start_ezsp(self):
"""Mark EZSP as running."""
Expand Down Expand Up @@ -576,7 +584,7 @@ async def write_config(self, config: dict) -> None:
# First, set the values
for cfg in ezsp_values.values():
# XXX: A read failure does not mean the value is not writeable!
status, current_value = await self.getValue(cfg.value_id)
status, current_value = await self.getValue(valueId=cfg.value_id)

if t.sl_Status.from_ember_status(status) == t.sl_Status.OK:
current_value, _ = type(cfg.value).deserialize(current_value)
Expand All @@ -590,7 +598,9 @@ async def write_config(self, config: dict) -> None:
current_value,
)

(status,) = await self.setValue(cfg.value_id, cfg.value.serialize())
(status,) = await self.setValue(
valueId=cfg.value_id, value=cfg.value.serialize()
)

if t.sl_Status.from_ember_status(status) != t.sl_Status.OK:
LOGGER.debug(
Expand All @@ -603,7 +613,9 @@ async def write_config(self, config: dict) -> None:

# Finally, set the config
for cfg in ezsp_config.values():
(status, current_value) = await self.getConfigurationValue(cfg.config_id)
(status, current_value) = await self.getConfigurationValue(
configId=cfg.config_id
)

# Only grow some config entries, all others should be set
if (
Expand All @@ -626,7 +638,9 @@ async def write_config(self, config: dict) -> None:
current_value,
)

(status,) = await self.setConfigurationValue(cfg.config_id, cfg.value)
(status,) = await self.setConfigurationValue(
configId=cfg.config_id, value=cfg.value
)
if t.sl_Status.from_ember_status(status) != t.sl_Status.OK:
LOGGER.debug(
"Could not set config %s = %s: %s",
Expand Down
30 changes: 19 additions & 11 deletions bellows/ezsp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@ def __init__(self, cb_handler: Callable, gateway: Gateway) -> None:
}
self.tc_policy = 0

def _ezsp_frame(self, name: str, *args: tuple[Any, ...]) -> bytes:
def _ezsp_frame(self, name: str, *args: Any, **kwargs: Any) -> bytes:
"""Serialize the named frame and data."""
c = self.COMMANDS[name]
c, tx_schema, rx_schema = self.COMMANDS[name]
frame = self._ezsp_frame_tx(name)
data = t.serialize(args, c[1])

if isinstance(tx_schema, dict):
data = t.serialize_dict(args, kwargs, tx_schema)
else:
data = tx_schema(*args, **kwargs).serialize()
return frame + data

@abc.abstractmethod
Expand All @@ -58,10 +62,10 @@ def _ezsp_frame_rx(self, data: bytes) -> tuple[int, int, bytes]:
def _ezsp_frame_tx(self, name: str) -> bytes:
"""Serialize the named frame."""

async def command(self, name, *args) -> Any:
async def command(self, name, *args, **kwargs) -> Any:
"""Serialize command and send it."""
LOGGER.debug("Sending command %s: %s", name, args)
data = self._ezsp_frame(name, *args)
LOGGER.debug("Sending command %s: %s %s", name, args, kwargs)
data = self._ezsp_frame(name, *args, **kwargs)
cmd_id, _, rx_schema = self.COMMANDS[name]
future = asyncio.get_running_loop().create_future()
self._awaiting[self._seq] = (cmd_id, rx_schema, future)
Expand Down Expand Up @@ -100,18 +104,22 @@ def __call__(self, data: bytes) -> None:
return

try:
if isinstance(rx_schema, tuple):
result, data = t.deserialize(data, rx_schema)
if isinstance(rx_schema, dict):
result, data = t.deserialize_dict(data, rx_schema)
LOGGER.debug("Received command %s: %s", frame_name, result)
result = list(result.values())
else:
result, data = rx_schema.deserialize(data)
LOGGER.debug("Received command %s: %s", frame_name, result)
except Exception:
LOGGER.warning(
"Failed to parse frame %s: %s", frame_name, binascii.hexlify(data)
"Failed to parse frame %s: %s",
frame_name,
binascii.hexlify(data),
exc_info=True,
)
raise

LOGGER.debug("Received command %s: %s", frame_name, result)

if data:
LOGGER.debug("Frame contains trailing data: %s", data)

Expand Down
4 changes: 2 additions & 2 deletions bellows/ezsp/v10/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class EZSPv10(EZSPv9):
async def write_child_data(self, children: dict[t.EUI64, t.NWK]) -> None:
for index, (eui64, nwk) in enumerate(children.items()):
await self.setChildData(
index,
t.EmberChildDataV10(
index=index,
child_data=t.EmberChildDataV10(
eui64=eui64,
type=t.EmberNodeType.SLEEPY_END_DEVICE,
id=nwk,
Expand Down
18 changes: 14 additions & 4 deletions bellows/ezsp/v10/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@
# Use the correct `EmberChildData` object with the extra field
"getChildData": (
0x004A,
(t.uint8_t,),
(t.EmberStatus, t.EmberChildDataV10),
{
"index": t.uint8_t,
},
{
"status": t.EmberStatus,
"child_data": t.EmberChildDataV10,
},
),
"setChildData": (
0x00AC,
(t.uint8_t, t.EmberChildDataV10),
(t.EmberStatus,),
{
"index": t.uint8_t,
"child_data": t.EmberChildDataV10,
},
{
"status": t.EmberStatus,
},
),
}
7 changes: 5 additions & 2 deletions bellows/ezsp/v11/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
**COMMANDS_v10,
"pollHandler": (
0x0044,
(),
tuple({"childId": t.EmberNodeId, "transmitExpected": t.Bool}.values()),
{},
{
"childId": t.EmberNodeId,
"transmitExpected": t.Bool,
},
),
}
Loading

0 comments on commit 8eb257d

Please sign in to comment.