test_protocol.py (4119B)
1 """Tests for inlite_ble protocol module.""" 2 3 from inlite_ble.protocol import ( 4 build_ack_payload, 5 build_block_data_payload, 6 build_discovery_payload, 7 build_unacknowledged_command_payload, 8 build_association_payload, 9 build_flush_payload, 10 build_outlet_mode_data, 11 OPCODE_SET_OUTLET_MODE, 12 OPCODE_DISCOVER, 13 OPCODE_IDENTIFY, 14 build_clock_payload, 15 build_legacy_datetime_payload, 16 ) 17 from datetime import UTC, datetime 18 19 20 class TestBuildOutletModeData: 21 """Tests for build_outlet_mode_data.""" 22 23 def test_turn_on(self) -> None: 24 result = build_outlet_mode_data(0, True) 25 assert result == bytes([0x00, 0x01, 0x01]) 26 27 def test_turn_off(self) -> None: 28 result = build_outlet_mode_data(0, False) 29 assert result == bytes([0x00, 0x00, 0x01]) 30 31 def test_zone_id_preserved(self) -> None: 32 result = build_outlet_mode_data(2, True) 33 assert result[0] == 2 34 35 def test_mode_mask_always_0x01(self) -> None: 36 for on in (True, False): 37 result = build_outlet_mode_data(0, on) 38 assert result[2] == 0x01 39 40 41 class TestBuildBlockDataPayload: 42 """Tests for build_block_data_payload.""" 43 44 def test_structure(self) -> None: 45 result = build_block_data_payload(OPCODE_SET_OUTLET_MODE, b"\x00\x01\x01") 46 # offset_lo, offset_hi, cmd_type=1, opcode_lo, opcode_hi, data... 47 assert result[0] == 0x00 # offset lo 48 assert result[1] == 0x00 # offset hi 49 assert result[2] == 0x01 # cmd_type 50 assert result[3] == OPCODE_SET_OUTLET_MODE & 0xFF 51 assert result[4] == (OPCODE_SET_OUTLET_MODE >> 8) & 0xFF 52 assert result[5:] == b"\x00\x01\x01" 53 54 def test_empty_data(self) -> None: 55 result = build_block_data_payload(0x0005, b"") 56 assert len(result) == 5 57 58 59 class TestBuildFlushPayload: 60 """Tests for build_flush_payload.""" 61 62 def test_zero(self) -> None: 63 assert build_flush_payload(0) == bytes([0x00, 0x00]) 64 65 def test_nonzero(self) -> None: 66 result = build_flush_payload(0x0105) 67 assert result == bytes([0x05, 0x01]) 68 69 70 class TestBuildAckPayload: 71 """Tests for build_ack_payload.""" 72 73 def test_without_end(self) -> None: 74 result = build_ack_payload(5, end=False) 75 assert result == bytes([0x05, 0x00]) 76 77 def test_with_end(self) -> None: 78 result = build_ack_payload(5, end=True) 79 assert result == bytes([0x05, 0x00, 0xEF]) 80 81 82 class TestBuildDiscoveryPayload: 83 """Tests for build_discovery_payload.""" 84 85 def test_structure(self) -> None: 86 result = build_discovery_payload() 87 assert result[0] == 0x01 88 assert result[1] == OPCODE_DISCOVER & 0xFF 89 assert result[2] == (OPCODE_DISCOVER >> 8) & 0xFF 90 91 92 class TestBuildUnacknowledgedCommandPayload: 93 def test_identify_structure(self) -> None: 94 assert build_unacknowledged_command_payload(OPCODE_IDENTIFY) == b"\x01\x14\x00" 95 96 97 class TestBuildAssociationPayload: 98 def test_structure(self) -> None: 99 key = bytes(range(16)) 100 assert build_association_payload(key) == b"\x01\x24\x00" + key 101 102 def test_rejects_wrong_key_size(self) -> None: 103 import pytest 104 with pytest.raises(ValueError): 105 build_association_payload(b"too short") 106 107 108 def test_identify_opcode_matches_official_app() -> None: 109 assert OPCODE_IDENTIFY == 0x0014 110 111 112 class TestClockPayloads: 113 def test_clock_payload_is_little_endian_with_quarter_hour_offset(self) -> None: 114 assert build_clock_payload(0x01020304, 3600) == b"\x04\x03\x02\x01\x04" 115 116 def test_clock_payload_supports_negative_offsets(self) -> None: 117 assert build_clock_payload(0, -5 * 3600) == b"\x00\x00\x00\x00\xec" 118 119 def test_clock_payload_appends_future_transitions(self) -> None: 120 assert build_clock_payload(1, 0, [(2, 3600)]) == b"\x01\x00\x00\x00\x00\x02\x00\x00\x00\x04" 121 122 def test_legacy_datetime_payload_is_utc(self) -> None: 123 timestamp = int(datetime(2026, 9, 3, 13, 14, 15, tzinfo=UTC).timestamp()) 124 assert build_legacy_datetime_payload(timestamp) == bytes([15, 14, 13, 3, 9, 0xEA, 0x07])