commit f666c87a7a0eaede04bb2dee18c1e1e34c3eee81 parent 3a7d32af9f85d5dcc34a400b514a64a51fcd6934 Author: Stéphan Kochen <git@stephank.nl> Date: Thu, 03 Sep 2026 21:58:01 +0200 Send identify as an unacknowledged command
diff --git a/custom_components/inlite/lib/inlite_ble/hub.py b/custom_components/inlite/lib/inlite_ble/hub.py index 7064a5766295daca484a26d92e486d89593bdac7..83204156eabe9a0f50acc09db3ee5aeb0b07b477 100644 --- a/custom_components/inlite/lib/inlite_ble/hub.py +++ b/custom_components/inlite/lib/inlite_ble/hub.py @@ -33,6 +33,7 @@ build_block_data_payload, build_flush_payload, build_ack_payload, build_discovery_payload, + build_unacknowledged_command_payload, build_association_payload, build_clock_payload, build_legacy_datetime_payload, @@ -675,7 +676,14 @@ ) async def identify(self) -> bool: """Ask a hub to briefly blink its identify light.""" - return await self._send_command(OPCODE_IDENTIFY, b"") + # The official app sends IDENTIFY as a fire-and-forget BLK_DATA_BLK + # packet. It has neither block-stream ACKs nor a response stream. + await self._write_mesh( + self._device_id, + PKT_BLOCK_DATA_BLK, + build_unacknowledged_command_payload(OPCODE_IDENTIFY), + ) + return True async def sync_clock( self, now: datetime, transitions: Iterable[tuple[int, int]] = () diff --git a/custom_components/inlite/lib/inlite_ble/protocol.py b/custom_components/inlite/lib/inlite_ble/protocol.py index d0f27e699d5cf5cfc323ccae35dd51220fc242a6..b447ef3745479e09c0d88a794d600d28c6ad63fe 100644 --- a/custom_components/inlite/lib/inlite_ble/protocol.py +++ b/custom_components/inlite/lib/inlite_ble/protocol.py @@ -76,7 +76,12 @@ def build_discovery_payload() -> bytes: """Build BLK_DATA_BLK discovery packet.""" - return bytes([0x01, OPCODE_DISCOVER & 0xFF, (OPCODE_DISCOVER >> 8) & 0xFF]) + return build_unacknowledged_command_payload(OPCODE_DISCOVER) + + +def build_unacknowledged_command_payload(opcode: int, command_data: bytes = b"") -> bytes: + """Build a fire-and-forget BLK_DATA_BLK command payload.""" + return bytes([0x01, opcode & 0xFF, (opcode >> 8) & 0xFF]) + command_data def build_association_payload(network_key: bytes) -> bytes: diff --git a/tests/test_hub.py b/tests/test_hub.py index 39a8f52a6a0395a20511f63e9e584562fa560db9..f44cb7597a3e7ee3ec0af6b4534623c294ca8877 100644 --- a/tests/test_hub.py +++ b/tests/test_hub.py @@ -6,6 +6,8 @@ from inlite_ble.hub import BLE_PACKET_PART_SIZE, STREAM_DATA_SIZE, InliteHub, ZoneState from inlite_ble.protocol import ( CHAR_CONTINUATION_UUID, CHAR_WRITE_UUID, + OPCODE_IDENTIFY, + PKT_BLOCK_DATA_BLK, PKT_BLOCK_FLUSH, PKT_BLOCK_STREAM, ) @@ -75,6 +77,20 @@ hub = InliteHub(device_id=1, passphrase="test") assert hub._accept_stream_segment(b"\x00\x00abc") assert not hub._accept_stream_segment(b"\x04\x00de") assert hub._stream_invalid is True + + def test_identify_is_fire_and_forget(self) -> None: + async def run() -> list[tuple[int, int, bytes]]: + hub = InliteHub(device_id=0x1234, passphrase="test") + writes: list[tuple[int, int, bytes]] = [] + + async def write_mesh(dest: int, packet_type: int, data: bytes) -> None: + writes.append((dest, packet_type, data)) + + hub._write_mesh = write_mesh # type: ignore[method-assign] + assert await hub.identify() + return writes + + assert asyncio.run(run()) == [(0x1234, PKT_BLOCK_DATA_BLK, b"\x01\x14\x00")] def test_response_stream_uses_literal_completion_marker(self) -> None: """The final ACK is count LE followed by ef, as captured from the app.""" diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 6fcb6284eb9335d1ceb0dbbda017328eaf0b1128..9b11d1022045c556ba880b5cb0f335943fc3b286 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -4,6 +4,7 @@ from inlite_ble.protocol import ( build_ack_payload, build_block_data_payload, build_discovery_payload, + build_unacknowledged_command_payload, build_association_payload, build_flush_payload, build_outlet_mode_data, @@ -86,6 +87,11 @@ result = build_discovery_payload() assert result[0] == 0x01 assert result[1] == OPCODE_DISCOVER & 0xFF assert result[2] == (OPCODE_DISCOVER >> 8) & 0xFF + + +class TestBuildUnacknowledgedCommandPayload: + def test_identify_structure(self) -> None: + assert build_unacknowledged_command_payload(OPCODE_IDENTIFY) == b"\x01\x14\x00" class TestBuildAssociationPayload: