ha-inlite

Home Assistant integration for in-lite
git clone https://git.stephank.nl/ha-inlite
Log | Files | Refs | README | LICENSE | ZIP

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])