diff --git a/tests/test_api.py b/tests/test_api.py index b9917a8..3288c5a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -67,10 +67,10 @@ def inner(command_id, params, rsp): def receiver(data): command, _ = deconz_api.Command.deserialize(data) - schema_with_defaults = deconz_api.TX_COMMANDS[command.command_id] + tx_schema, _ = deconz_api.COMMAND_SCHEMAS[command.command_id] schema = {} - for k, v in schema_with_defaults.items(): + for k, v in tx_schema.items(): if v in (deconz_api.FRAME_LENGTH, deconz_api.PAYLOAD_LENGTH): v = t.uint16_t elif not inspect.isclass(v): @@ -82,15 +82,14 @@ def receiver(data): for params, ret in receiver._handlers[command.command_id]: if all(kwargs[k] == v for k, v in params.items()): + _, rx_schema = deconz_api.COMMAND_SCHEMAS[command.command_id] + asyncio.get_running_loop().call_soon( gateway._api.data_received, deconz_api.Command( command_id=command.command_id, seq=command.seq, - payload=t.serialize_dict( - ret, - deconz_api.RX_COMMANDS[command.command_id][0], - ), + payload=t.serialize_dict(ret, rx_schema), ).serialize(), ) @@ -118,14 +117,10 @@ async def test_close(api): def test_commands(): - for cmd, (schema, solicited) in deconz_api.RX_COMMANDS.items(): - assert isinstance(schema, dict) + for cmd, (tx_schema, rx_schema) in deconz_api.COMMAND_SCHEMAS.items(): assert isinstance(cmd, deconz_api.CommandId) - assert isinstance(solicited, bool) - - for cmd, schema in deconz_api.TX_COMMANDS.items(): - assert isinstance(cmd, deconz_api.CommandId) is True - assert isinstance(schema, dict) is True + assert isinstance(tx_schema, dict) or tx_schema is None + assert isinstance(rx_schema, dict) async def test_command(api): @@ -170,7 +165,7 @@ async def test_command(api): seq=api._seq, payload=t.serialize_dict( params, - deconz_api.RX_COMMANDS[deconz_api.CommandId.aps_data_indication][0], + deconz_api.COMMAND_SCHEMAS[deconz_api.CommandId.aps_data_indication][1], ), ).serialize() diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index 4f6e9c9..9b6f98e 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -186,80 +186,17 @@ class Command(Struct): payload: t.Bytes -TX_COMMANDS = { - CommandId.add_neighbour: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - "payload_length": PAYLOAD_LENGTH, - "unknown": t.uint8_t, - "nwk": t.NWK, - "ieee": t.EUI64, - "mac_capability_flags": t.uint8_t, - }, - CommandId.aps_data_confirm: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - "payload_length": PAYLOAD_LENGTH, - }, - CommandId.aps_data_indication: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - "payload_length": PAYLOAD_LENGTH, - "flags": t.DataIndicationFlags, - }, - CommandId.aps_data_request: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - "payload_length": PAYLOAD_LENGTH, - "request_id": t.uint8_t, - "flags": t.DeconzSendDataFlags, - "dst": t.DeconzAddressEndpoint, - "profile_id": t.uint16_t, - "cluster_id": t.uint16_t, - "src_ep": t.uint8_t, - "asdu": t.LongOctetString, - "tx_options": t.DeconzTransmitOptions, - "radius": t.uint8_t, - "relays": t.NWKList, # optional - }, - CommandId.change_network_state: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - # "payload_length": PAYLOAD_LENGTH, - "network_state": NetworkState, - }, - CommandId.device_state: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - # "payload_length": PAYLOAD_LENGTH, - "reserved1": t.uint8_t(0), - "reserved2": t.uint8_t(0), - "reserved3": t.uint8_t(0), - }, - CommandId.read_parameter: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - "payload_length": PAYLOAD_LENGTH, - "parameter_id": NetworkParameter, - "parameter": t.Bytes, - }, - CommandId.version: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - # "payload_length": PAYLOAD_LENGTH, - "reserved": t.uint32_t(0), - }, - CommandId.write_parameter: { - "status": Status.SUCCESS, - "frame_length": FRAME_LENGTH, - "payload_length": PAYLOAD_LENGTH, - "parameter_id": NetworkParameter, - "parameter": t.Bytes, - }, -} - -RX_COMMANDS = { +COMMAND_SCHEMAS = { CommandId.add_neighbour: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + "payload_length": PAYLOAD_LENGTH, + "unknown": t.uint8_t, + "nwk": t.NWK, + "ieee": t.EUI64, + "mac_capability_flags": t.uint8_t, + }, { "status": Status, "frame_length": t.uint16_t, @@ -269,9 +206,13 @@ class Command(Struct): "ieee": t.EUI64, "mac_capability_flags": t.uint8_t, }, - True, ), CommandId.aps_data_confirm: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + "payload_length": PAYLOAD_LENGTH, + }, { "status": Status, "frame_length": t.uint16_t, @@ -286,9 +227,14 @@ class Command(Struct): "reserved3": t.uint8_t, "reserved4": t.uint8_t, }, - True, ), CommandId.aps_data_indication: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + "payload_length": PAYLOAD_LENGTH, + "flags": t.DataIndicationFlags, + }, { "status": Status, "frame_length": t.uint16_t, @@ -310,9 +256,23 @@ class Command(Struct): "reserved6": t.uint8_t, "rssi": t.int8s, }, - True, ), CommandId.aps_data_request: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + "payload_length": PAYLOAD_LENGTH, + "request_id": t.uint8_t, + "flags": t.DeconzSendDataFlags, + "dst": t.DeconzAddressEndpoint, + "profile_id": t.uint16_t, + "cluster_id": t.uint16_t, + "src_ep": t.uint8_t, + "asdu": t.LongOctetString, + "tx_options": t.DeconzTransmitOptions, + "radius": t.uint8_t, + "relays": t.NWKList, # optional + }, { "status": Status, "frame_length": t.uint16_t, @@ -320,18 +280,30 @@ class Command(Struct): "device_state": DeviceState, "request_id": t.uint8_t, }, - True, ), CommandId.change_network_state: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + # "payload_length": PAYLOAD_LENGTH, + "network_state": NetworkState, + }, { "status": Status, "frame_length": t.uint16_t, # "payload_length": t.uint16_t, "network_state": NetworkState, }, - True, ), CommandId.device_state: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + # "payload_length": PAYLOAD_LENGTH, + "reserved1": t.uint8_t(0), + "reserved2": t.uint8_t(0), + "reserved3": t.uint8_t(0), + }, { "status": Status, "frame_length": t.uint16_t, @@ -340,9 +312,9 @@ class Command(Struct): "reserved1": t.uint8_t, "reserved2": t.uint8_t, }, - True, ), CommandId.device_state_changed: ( + None, { "status": Status, "frame_length": t.uint16_t, @@ -350,9 +322,9 @@ class Command(Struct): "device_state": DeviceState, "reserved": t.uint8_t, }, - False, ), CommandId.mac_poll: ( + None, { "status": Status, "frame_length": t.uint16_t, @@ -363,58 +335,60 @@ class Command(Struct): "life_time": t.uint32_t, # Optional "device_timeout": t.uint32_t, # Optional }, - False, ), CommandId.read_parameter: ( { - "status": Status, - "frame_length": t.uint16_t, - "payload_length": t.uint16_t, + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + "payload_length": PAYLOAD_LENGTH, "parameter_id": NetworkParameter, "parameter": t.Bytes, }, - True, - ), - CommandId.mac_beacon_indication: ( { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, - "src_addr": t.uint16_t, - "pan_id": t.uint16_t, - "channel": t.uint8_t, - "flags": t.uint8_t, - "update_id": t.uint8_t, - "data": t.Bytes, + "parameter_id": NetworkParameter, + "parameter": t.Bytes, }, - False, ), CommandId.version: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + # "payload_length": PAYLOAD_LENGTH, + "reserved": t.uint32_t(0), + }, { "status": Status, "frame_length": t.uint16_t, # "payload_length": t.uint16_t, "version": t.uint32_t, }, - True, ), CommandId.write_parameter: ( + { + "status": Status.SUCCESS, + "frame_length": FRAME_LENGTH, + "payload_length": PAYLOAD_LENGTH, + "parameter_id": NetworkParameter, + "parameter": t.Bytes, + }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "parameter_id": NetworkParameter, }, - True, ), CommandId.zigbee_green_power: ( + None, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "reserved": t.LongOctetString, }, - False, ), } @@ -495,10 +469,10 @@ def close(self): async def _command(self, cmd, **kwargs): payload = [] - schema = TX_COMMANDS[cmd] + tx_schema, _ = COMMAND_SCHEMAS[cmd] trailing_optional = False - for name, param_type in schema.items(): + for name, param_type in tx_schema.items(): if isinstance(param_type, int): if name not in kwargs: # Default value @@ -578,19 +552,16 @@ async def _command(self, cmd, **kwargs): def data_received(self, data: bytes) -> None: command, _ = Command.deserialize(data) - if command.command_id not in RX_COMMANDS: + if command.command_id not in COMMAND_SCHEMAS: LOGGER.warning("Unknown command received: %s", command) return - schema, solicited = RX_COMMANDS[command.command_id] + _, rx_schema = COMMAND_SCHEMAS[command.command_id] - if solicited and command.seq in self._awaiting: - fut, cmd = self._awaiting.pop(command.seq) - else: - fut, cmd = None, None + fut, cmd = self._awaiting.pop(command.seq, (None, None)) try: - params, rest = t.deserialize_dict(command.payload, schema) + params, rest = t.deserialize_dict(command.payload, rx_schema) if rest: LOGGER.debug("Unparsed data remains after frame: %s, %s", command, rest) @@ -622,30 +593,33 @@ def data_received(self, data: bytes) -> None: ) status = params["status"] - if status != Status.SUCCESS: + if cmd != command.command_id: + exc = CommandError( + status, + ( + f"Received invalid response {command.command_id}{params}" + f" to request {cmd}" + ), + ) + elif status != Status.SUCCESS: + exc = CommandError(status, f"{command.command_id}, status: {status}") + else: + exc = None + + if fut is not None: try: - fut.set_exception( - CommandError(status, f"{command.command_id}, status: {status}") - ) + if exc is None: + fut.set_result(params) + else: + fut.set_exception(exc) except asyncio.InvalidStateError: LOGGER.warning( "Duplicate or delayed response for 0x:%02x sequence", command.seq, ) - return - if fut is not None: - if cmd != command.command_id: - LOGGER.warning( - "UNEXPECTED RESPONSE TYPE???? %s != %s", cmd, command.command_id - ) - - try: - fut.set_result(params) - except asyncio.InvalidStateError: - LOGGER.warning( - "Duplicate or delayed response for 0x:%02x sequence", command.seq - ) + if exc is not None: + return if handler := getattr(self, f"_handle_{command.command_id.name}", None): handler_params = {