casambi-bt-revamped 0.3.12.dev8__tar.gz → 0.3.12.dev10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {casambi_bt_revamped-0.3.12.dev8/src/casambi_bt_revamped.egg-info → casambi_bt_revamped-0.3.12.dev10}/PKG-INFO +1 -1
  2. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/setup.cfg +1 -1
  3. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_casambi.py +26 -9
  4. casambi_bt_revamped-0.3.12.dev10/src/CasambiBt/_classic_crypto.py +146 -0
  5. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_client.py +339 -17
  6. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_constants.py +9 -0
  7. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_network.py +26 -19
  8. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_version.py +1 -1
  9. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10/src/casambi_bt_revamped.egg-info}/PKG-INFO +1 -1
  10. casambi_bt_revamped-0.3.12.dev10/tests/test_classic_protocol.py +385 -0
  11. casambi_bt_revamped-0.3.12.dev8/src/CasambiBt/_classic_crypto.py +0 -31
  12. casambi_bt_revamped-0.3.12.dev8/tests/test_classic_protocol.py +0 -157
  13. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/LICENSE +0 -0
  14. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/README.md +0 -0
  15. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/pyproject.toml +0 -0
  16. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/__init__.py +0 -0
  17. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_cache.py +0 -0
  18. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_discover.py +0 -0
  19. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_encryption.py +0 -0
  20. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_invocation.py +0 -0
  21. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_keystore.py +0 -0
  22. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_operation.py +0 -0
  23. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_switch_events.py +0 -0
  24. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_unit.py +0 -0
  25. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/errors.py +0 -0
  26. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/py.typed +0 -0
  27. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/SOURCES.txt +0 -0
  28. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/dependency_links.txt +0 -0
  29. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
  30. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/top_level.txt +0 -0
  31. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/tests/test_legacy_protocol_handling.py +0 -0
  32. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/tests/test_switch_event_logs.py +0 -0
  33. {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/tests/test_unit_state_logs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev8
3
+ Version: 0.3.12.dev10
4
4
  Summary: Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
5
5
  Home-page: https://github.com/rankjie/casambi-bt
6
6
  Author: rankjie
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = casambi-bt-revamped
3
- version = 0.3.12.dev8
3
+ version = 0.3.12.dev10
4
4
  author = rankjie
5
5
  author_email = rankjie@gmail.com
6
6
  description = Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
@@ -206,16 +206,33 @@ class Casambi:
206
206
 
207
207
  # Classic protocol uses signed command frames (u1.C1753e / u1.EnumC1754f).
208
208
  if self._casaClient is not None and self._casaClient.protocolMode == ProtocolMode.CLASSIC:
209
- # EnumC1754f ordinals (ground truth: casambi-android u1.EnumC1754f):
210
- # - AllUnitsLevel=4, UnitLevel=7, GroupLevel=26
211
- if isinstance(target, Unit):
212
- cmd = self._casaClient.buildClassicCommand(7, payload, target_id=target.deviceId)
213
- elif isinstance(target, Group):
214
- cmd = self._casaClient.buildClassicCommand(26, payload, target_id=target.groudId)
215
- elif target is None:
216
- cmd = self._casaClient.buildClassicCommand(4, payload)
209
+ # Check if we should use the alternative simple format from BLE captures
210
+ import os
211
+ use_simple = os.environ.get("CASAMBI_BT_CLASSIC_FORMAT", "").lower() == "simple"
212
+
213
+ if use_simple:
214
+ # Simple format: [counter][unit_id][param_len][dimmer]
215
+ # For "all units", use unit_id=0xFF
216
+ if isinstance(target, Unit):
217
+ cmd = self._casaClient.buildClassicCommandSimple(target.deviceId, level)
218
+ elif isinstance(target, Group):
219
+ # Groups in simple format: not fully confirmed, try group ID
220
+ cmd = self._casaClient.buildClassicCommandSimple(target.groudId, level)
221
+ elif target is None:
222
+ cmd = self._casaClient.buildClassicCommandSimple(0xFF, level)
223
+ else:
224
+ raise TypeError(f"Unkown target type {type(target)}")
217
225
  else:
218
- raise TypeError(f"Unkown target type {type(target)}")
226
+ # EnumC1754f ordinals (ground truth: casambi-android u1.EnumC1754f):
227
+ # - AllUnitsLevel=4, UnitLevel=7, GroupLevel=26
228
+ if isinstance(target, Unit):
229
+ cmd = self._casaClient.buildClassicCommand(7, payload, target_id=target.deviceId)
230
+ elif isinstance(target, Group):
231
+ cmd = self._casaClient.buildClassicCommand(26, payload, target_id=target.groudId)
232
+ elif target is None:
233
+ cmd = self._casaClient.buildClassicCommand(4, payload)
234
+ else:
235
+ raise TypeError(f"Unkown target type {type(target)}")
219
236
 
220
237
  await self._casaClient.send(cmd)
221
238
  return
@@ -0,0 +1,146 @@
1
+ """Classic Casambi protocol helpers (CMAC signing/verification).
2
+
3
+ Ground truth:
4
+ - casambi-android `t1.P.o(...)` calculates a CMAC over:
5
+ connection_hash[0:8] + payload
6
+ and stores the CMAC (prefix) into the packet header.
7
+
8
+ The CMAC input structure from t1.P.o():
9
+ kVar.i(rVar.f16699L, 0, 8); // connection_hash[0:8]
10
+ kVar.i(b(), this.f16551t + i9, (c() - i9) - this.f16551t); // payload starting at seq offset
11
+
12
+ For "conformant" classic (rVar.Z=true):
13
+ CMAC input = conn_hash[0:8] || seq(2 bytes BE) || command_bytes
14
+
15
+ For "legacy" classic (rVar.Z=false):
16
+ CMAC input = conn_hash[0:8] || command_bytes
17
+
18
+ Test vectors based on community BLE captures (GitHub lkempf/casambi-bt#17):
19
+
20
+ Capture from sMauldaeschle:
21
+ Raw packet: 0215db43c40004040200b3
22
+ - Byte 0: 02 (auth level = visitor)
23
+ - Bytes 1-4: 15db43c4 (4-byte CMAC signature)
24
+ - Byte 5: 00 (padding/marker)
25
+ - Byte 6: 04 (counter/sequence)
26
+ - Byte 7: 04 (unit id)
27
+ - Byte 8: 02 (length of parameters)
28
+ - Byte 9: 00 (dimmer value)
29
+ - Byte 10: b3 (temperature/vertical value)
30
+
31
+ Capture from FliegenKLATSCH:
32
+ Raw packet: 0200 43A2 9600 0203 0254 FF
33
+ - Bytes 0-1: 0200 (auth 02, then...)
34
+ - Bytes 2-5: 43A2 9600 (first 4 bytes of CMAC)
35
+ - The CMAC input was: <8 bytes connection hash> 00 0203 0254 FF
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import logging
41
+ from typing import TYPE_CHECKING
42
+
43
+ from cryptography.hazmat.primitives.cmac import CMAC
44
+ from cryptography.hazmat.primitives.ciphers.algorithms import AES
45
+
46
+ if TYPE_CHECKING:
47
+ pass
48
+
49
+ _logger = logging.getLogger(__name__)
50
+
51
+
52
+ def classic_cmac(key: bytes, conn_hash8: bytes, payload: bytes, *, debug: bool = False) -> bytes:
53
+ """Compute the Classic CMAC (16 bytes) over connection hash + payload.
54
+
55
+ Args:
56
+ key: 16-byte AES key (visitor or manager key from cloud)
57
+ conn_hash8: First 8 bytes of the connection hash read from CA51/auth char
58
+ payload: The data to sign (for conformant: seq + command; for legacy: command only)
59
+ debug: If True, log CMAC input/output for debugging
60
+
61
+ Returns:
62
+ 16-byte CMAC
63
+ """
64
+ if len(conn_hash8) != 8:
65
+ raise ValueError("conn_hash8 must be 8 bytes")
66
+
67
+ cmac_input = conn_hash8 + payload
68
+ cmac = CMAC(AES(key))
69
+ cmac.update(cmac_input)
70
+ result = cmac.finalize()
71
+
72
+ if debug:
73
+ _logger.warning(
74
+ "[CLASSIC_CMAC_DEBUG] key_len=%d conn_hash8=%s payload_len=%d payload_prefix=%s cmac=%s",
75
+ len(key),
76
+ conn_hash8.hex(),
77
+ len(payload),
78
+ payload[:16].hex() if len(payload) > 16 else payload.hex(),
79
+ result.hex(),
80
+ )
81
+
82
+ return result
83
+
84
+
85
+ def classic_cmac_prefix(
86
+ key: bytes, conn_hash8: bytes, payload: bytes, prefix_len: int, *, debug: bool = False
87
+ ) -> bytes:
88
+ """Return the prefix bytes that are embedded into the Classic packet header.
89
+
90
+ Args:
91
+ key: 16-byte AES key
92
+ conn_hash8: First 8 bytes of connection hash
93
+ payload: Data to sign
94
+ prefix_len: Number of CMAC bytes to use (4 for visitor, 16 for manager)
95
+ debug: If True, log CMAC computation
96
+
97
+ Returns:
98
+ First prefix_len bytes of the CMAC
99
+ """
100
+ mac = classic_cmac(key, conn_hash8, payload, debug=debug)
101
+ return mac[:prefix_len]
102
+
103
+
104
+ def verify_classic_cmac(
105
+ key: bytes, conn_hash8: bytes, payload: bytes, expected_prefix: bytes
106
+ ) -> bool:
107
+ """Verify a Classic CMAC signature prefix.
108
+
109
+ Args:
110
+ key: 16-byte AES key
111
+ conn_hash8: First 8 bytes of connection hash
112
+ payload: Data that was signed
113
+ expected_prefix: The CMAC prefix from the packet header
114
+
115
+ Returns:
116
+ True if the computed CMAC prefix matches expected_prefix
117
+ """
118
+ computed = classic_cmac_prefix(key, conn_hash8, payload, len(expected_prefix))
119
+ return computed == expected_prefix
120
+
121
+
122
+ # Test vectors from RFC 4493 (AES-CMAC) - used in test_classic_protocol.py
123
+ # These confirm our CMAC implementation is correct.
124
+ RFC4493_TEST_KEY = bytes.fromhex("2b7e151628aed2a6abf7158809cf4f3c")
125
+ RFC4493_VECTORS = [
126
+ # (message, expected_cmac)
127
+ (bytes.fromhex("6bc1bee22e409f96e93d7e117393172a"),
128
+ bytes.fromhex("070a16b46b4d4144f79bdd9dd04a287c")),
129
+ (bytes.fromhex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51"),
130
+ bytes.fromhex("ce0cbf1738f4df6428b1d93bf12081c9")),
131
+ ]
132
+
133
+ # Placeholder for real Casambi test vectors once we have them from captures
134
+ # Format: (visitor_key, conn_hash8, payload_after_header, expected_cmac_prefix_4, description)
135
+ CASAMBI_CLASSIC_TEST_VECTORS: list[tuple[bytes, bytes, bytes, bytes, str]] = [
136
+ # These will be populated from real BLE captures
137
+ # Example structure (not verified yet):
138
+ # (
139
+ # bytes.fromhex("...16 byte key..."),
140
+ # bytes.fromhex("...8 byte conn hash..."),
141
+ # bytes.fromhex("...payload after auth byte..."),
142
+ # bytes.fromhex("...4 byte expected cmac..."),
143
+ # "Description of what this packet does"
144
+ # ),
145
+ ]
146
+