casambi-bt-revamped 0.3.12.dev9__py3-none-any.whl → 0.3.12.dev10__py3-none-any.whl

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.
CasambiBt/_casambi.py CHANGED
@@ -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
@@ -4,28 +4,143 @@ Ground truth:
4
4
  - casambi-android `t1.P.o(...)` calculates a CMAC over:
5
5
  connection_hash[0:8] + payload
6
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
7
36
  """
8
37
 
9
38
  from __future__ import annotations
10
39
 
40
+ import logging
41
+ from typing import TYPE_CHECKING
42
+
11
43
  from cryptography.hazmat.primitives.cmac import CMAC
12
44
  from cryptography.hazmat.primitives.ciphers.algorithms import AES
13
45
 
46
+ if TYPE_CHECKING:
47
+ pass
48
+
49
+ _logger = logging.getLogger(__name__)
50
+
14
51
 
15
- def classic_cmac(key: bytes, conn_hash8: bytes, payload: bytes) -> bytes:
16
- """Compute the Classic CMAC (16 bytes) over connection hash + payload."""
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
+ """
17
64
  if len(conn_hash8) != 8:
18
65
  raise ValueError("conn_hash8 must be 8 bytes")
66
+
67
+ cmac_input = conn_hash8 + payload
19
68
  cmac = CMAC(AES(key))
20
- cmac.update(conn_hash8)
21
- cmac.update(payload)
22
- return cmac.finalize()
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
23
83
 
24
84
 
25
85
  def classic_cmac_prefix(
26
- key: bytes, conn_hash8: bytes, payload: bytes, prefix_len: int
86
+ key: bytes, conn_hash8: bytes, payload: bytes, prefix_len: int, *, debug: bool = False
27
87
  ) -> bytes:
28
- """Return the prefix bytes that are embedded into the Classic packet header."""
29
- mac = classic_cmac(key, conn_hash8, payload)
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)
30
101
  return mac[:prefix_len]
31
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
+
CasambiBt/_client.py CHANGED
@@ -163,6 +163,11 @@ class CasambiClient:
163
163
  self._classicRxKindSamples: dict[str, int] = {}
164
164
  self._classicRxLastStatsTs = time.monotonic()
165
165
 
166
+ # Classic diagnostic packet history (for dump_classic_diagnostics service)
167
+ self._classicTxHistory: list[dict[str, Any]] = []
168
+ self._classicRxHistory: list[dict[str, Any]] = []
169
+ self._classicDiagMaxHistory = 50 # Keep last 50 TX and RX packets
170
+
166
171
  @property
167
172
  def protocolMode(self) -> ProtocolMode | None:
168
173
  return self._protocolMode
@@ -1004,6 +1009,39 @@ class CasambiClient:
1004
1009
 
1005
1010
  return bytes(b)
1006
1011
 
1012
+ def buildClassicCommandSimple(
1013
+ self,
1014
+ unit_id: int,
1015
+ dimmer: int,
1016
+ extra: int | None = None,
1017
+ ) -> bytes:
1018
+ """Build a Classic command using the simple format from BLE captures.
1019
+
1020
+ This alternative format was observed in real BLE captures and differs from
1021
+ the Android u1.C1753e command record format. Use with env variable
1022
+ CASAMBI_BT_CLASSIC_FORMAT=simple to experiment.
1023
+
1024
+ Format (before header added by _sendClassic):
1025
+ [counter:1][unit_id:1][param_len:1][dimmer:1][extra:1?]
1026
+
1027
+ The header (added by _sendClassic) is:
1028
+ - Conformant: [auth:1][cmac:4|16][seq:2]
1029
+ - Legacy: [cmac:4]
1030
+
1031
+ Args:
1032
+ unit_id: Target unit ID (0-255, use 0xFF for "all units")
1033
+ dimmer: Dimmer/level value (0-255)
1034
+ extra: Optional extra parameter (e.g., temperature/vertical value)
1035
+
1036
+ Returns:
1037
+ Command bytes to pass to _sendClassic
1038
+ """
1039
+ counter = self._classic_next_div()
1040
+ if extra is not None:
1041
+ return bytes([counter, unit_id & 0xFF, 2, dimmer & 0xFF, extra & 0xFF])
1042
+ else:
1043
+ return bytes([counter, unit_id & 0xFF, 1, dimmer & 0xFF])
1044
+
1007
1045
  async def _sendClassic(self, command_bytes: bytes) -> None:
1008
1046
  self._checkState(ConnectionState.AUTHENTICATED)
1009
1047
  if self._protocolMode != ProtocolMode.CLASSIC:
@@ -1143,7 +1181,46 @@ class CasambiClient:
1143
1181
 
1144
1182
  # Classic packets can exceed 20 bytes when using a 16-byte manager signature.
1145
1183
  # Bleak needs a write-with-response for long writes on most backends.
1146
- await self._gattClient.write_gatt_char(tx_uuid, bytes(pkt), response=True)
1184
+ tx_result = "pending"
1185
+ try:
1186
+ await self._gattClient.write_gatt_char(tx_uuid, bytes(pkt), response=True)
1187
+ tx_result = "ok"
1188
+ except Exception as e:
1189
+ tx_result = f"error: {type(e).__name__}: {e}"
1190
+ raise
1191
+ finally:
1192
+ # Record TX in diagnostic history
1193
+ tx_entry = {
1194
+ "timestamp": time.monotonic(),
1195
+ "header_mode": header_mode,
1196
+ "key": key_name,
1197
+ "signed": signed,
1198
+ "tx_uuid": tx_uuid,
1199
+ "auth_level": auth_level if header_mode == "conformant" else None,
1200
+ "sig_len": sig_len,
1201
+ "seq": seq,
1202
+ "cmd_ordinal": cmd_ordinal,
1203
+ "cmd_target": cmd_target,
1204
+ "cmd_div": cmd_div,
1205
+ "cmd_lifetime": cmd_lifetime,
1206
+ "cmd_payload_len": cmd_payload_len,
1207
+ "total_len": len(pkt),
1208
+ "pre_sign_hex": b2a(command_bytes).decode("ascii"),
1209
+ "post_sign_hex": b2a(bytes(pkt)).decode("ascii"),
1210
+ "result": tx_result,
1211
+ }
1212
+ self._classicTxHistory.append(tx_entry)
1213
+ if len(self._classicTxHistory) > self._classicDiagMaxHistory:
1214
+ self._classicTxHistory = self._classicTxHistory[-self._classicDiagMaxHistory:]
1215
+
1216
+ # Enhanced TX diagnostic log
1217
+ self._logger.warning(
1218
+ "[CLASSIC_DIAG_TX_RESULT] result=%s header=%s seq=%s total_len=%d",
1219
+ tx_result,
1220
+ header_mode,
1221
+ None if seq is None else f"0x{seq:04x}",
1222
+ len(pkt),
1223
+ )
1147
1224
 
1148
1225
  def _establishedNofityCallback(
1149
1226
  self, handle: BleakGATTCharacteristic, data: bytes
@@ -1231,10 +1308,26 @@ class CasambiClient:
1231
1308
  """
1232
1309
  self._inPacketCount += 1
1233
1310
  self._classicRxFrames += 1
1311
+ rx_ts = time.monotonic()
1234
1312
  if self._classicFirstRxTs is None:
1235
- self._classicFirstRxTs = time.monotonic()
1313
+ self._classicFirstRxTs = rx_ts
1236
1314
 
1237
1315
  raw = bytes(data)
1316
+
1317
+ # Enhanced RX diagnostic logging
1318
+ try:
1319
+ handle_uuid = str(getattr(handle, "uuid", "unknown")).lower()
1320
+ except Exception:
1321
+ handle_uuid = "unknown"
1322
+
1323
+ self._logger.warning(
1324
+ "[CLASSIC_DIAG_RX] #%d handle=%s len=%d hex=%s",
1325
+ self._classicRxFrames,
1326
+ handle_uuid,
1327
+ len(raw),
1328
+ b2a(raw[: min(len(raw), 48)]).decode("ascii") + ("..." if len(raw) > 48 else ""),
1329
+ )
1330
+
1238
1331
  if self._logger.isEnabledFor(logging.DEBUG):
1239
1332
  self._logger.debug(
1240
1333
  "[CASAMBI_CLASSIC_RX_RAW] len=%d hex=%s",
@@ -1424,6 +1517,36 @@ class CasambiClient:
1424
1517
  elif verified is None:
1425
1518
  self._classicRxUnverifiable += 1
1426
1519
 
1520
+ # Record RX in diagnostic history
1521
+ rx_entry = {
1522
+ "timestamp": rx_ts,
1523
+ "handle_uuid": handle_uuid,
1524
+ "header_mode": best["mode"],
1525
+ "verified": verified,
1526
+ "auth_level": best["auth_level"],
1527
+ "sig_len": best["sig_len"],
1528
+ "seq": best["seq"],
1529
+ "payload_len": len(payload),
1530
+ "raw_hex": b2a(raw).decode("ascii"),
1531
+ "payload_hex": b2a(payload).decode("ascii"),
1532
+ "score": best["score"],
1533
+ }
1534
+ self._classicRxHistory.append(rx_entry)
1535
+ if len(self._classicRxHistory) > self._classicDiagMaxHistory:
1536
+ self._classicRxHistory = self._classicRxHistory[-self._classicDiagMaxHistory:]
1537
+
1538
+ # Enhanced RX parse result log
1539
+ self._logger.warning(
1540
+ "[CLASSIC_DIAG_RX_PARSE] mode=%s verified=%s auth=%s sig_len=%d seq=%s score=%d payload_len=%d",
1541
+ best["mode"],
1542
+ verified,
1543
+ None if best["auth_level"] is None else f"0x{best['auth_level']:02x}",
1544
+ best["sig_len"],
1545
+ None if best["seq"] is None else f"0x{best['seq']:04x}",
1546
+ best["score"],
1547
+ len(payload),
1548
+ )
1549
+
1427
1550
  # Auto-correct header mode if the other format parses much better.
1428
1551
  if best["mode"] != preferred:
1429
1552
  # Only switch if we got a stronger signal (verified or plausible payload with fewer assumptions).
@@ -1865,3 +1988,39 @@ class CasambiClient:
1865
1988
 
1866
1989
  self._connectionState = ConnectionState.NONE
1867
1990
  self._logger.info("Disconnected.")
1991
+
1992
+ def getClassicDiagnostics(self) -> dict[str, Any]:
1993
+ """Return Classic protocol diagnostic state for external services.
1994
+
1995
+ This method provides a snapshot of Classic protocol state including:
1996
+ - Connection parameters (hash, mode, UUIDs)
1997
+ - RX/TX statistics
1998
+ - Last N TX and RX packets
1999
+ - Any detected errors or anomalies
2000
+
2001
+ Safe to call from HA services for dump_classic_diagnostics.
2002
+ """
2003
+ return {
2004
+ "protocol_mode": self._protocolMode.name if self._protocolMode else None,
2005
+ "classic_header_mode": self._classicHeaderMode,
2006
+ "classic_hash_source": self._classicHashSource,
2007
+ "classic_conn_hash8_hex": b2a(self._classicConnHash8).decode("ascii") if self._classicConnHash8 else None,
2008
+ "classic_tx_uuid": self._classicTxCharUuid,
2009
+ "classic_notify_uuids": sorted(self._classicNotifyCharUuids) if self._classicNotifyCharUuids else [],
2010
+ "classic_first_rx_ts": self._classicFirstRxTs,
2011
+ "classic_rx_stats": {
2012
+ "frames": self._classicRxFrames,
2013
+ "verified": self._classicRxVerified,
2014
+ "unverifiable": self._classicRxUnverifiable,
2015
+ "parse_fail": self._classicRxParseFail,
2016
+ "type6_unitstate": self._classicRxType6,
2017
+ "type7_switch": self._classicRxType7,
2018
+ "type9_netconf": self._classicRxType9,
2019
+ "cmdstream": self._classicRxCmdStream,
2020
+ "unknown": self._classicRxUnknown,
2021
+ },
2022
+ "classic_tx_count": len(self._classicTxHistory),
2023
+ "classic_rx_count": len(self._classicRxHistory),
2024
+ "classic_tx_history": self._classicTxHistory[-20:], # Last 20
2025
+ "classic_rx_history": self._classicRxHistory[-20:], # Last 20
2026
+ }
CasambiBt/_version.py CHANGED
@@ -7,4 +7,4 @@ Avoid using importlib.metadata in hot paths by providing a static version string
7
7
  __all__ = ["__version__"]
8
8
 
9
9
  # NOTE: Must match `casambi-bt/setup.cfg` [metadata] version.
10
- __version__ = "0.3.12.dev9"
10
+ __version__ = "0.3.12.dev10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev9
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,8 +1,8 @@
1
1
  CasambiBt/__init__.py,sha256=iJdTF4oeXfj5d5gfGxQkacqUjtnQo0IW-zFPJvFjWWk,336
2
2
  CasambiBt/_cache.py,sha256=3bQil8vhSy4f4sf9JusMfEdQC7d3cJuva9qHhyKro-0,3808
3
- CasambiBt/_casambi.py,sha256=TN4ecgjm95nSJ4h9TsKayNn577Y82fdsGK4IGUZF23Q,40666
4
- CasambiBt/_classic_crypto.py,sha256=6DcCOdjLQo7k2cOOutNdUKupykOG_E2TDDwg6fH-ODM,998
5
- CasambiBt/_client.py,sha256=yn6DckDCKap1YJ0xyDhk5wlvE6wvPIUGPe9M1veRDVs,77317
3
+ CasambiBt/_casambi.py,sha256=9pWTxR1ZBARK-IB91PSRAQrcRoswU40jI-9AfLPpvW0,41649
4
+ CasambiBt/_classic_crypto.py,sha256=XIp3JBaeY8hIUv5kB0ygVG_eRx9AgHHF4ts2--CFm78,4973
5
+ CasambiBt/_client.py,sha256=EuaCwb3t6D0sbQR6XA84UWDEtkc3n7WBRozsM2kFu-Y,83927
6
6
  CasambiBt/_constants.py,sha256=86heoDdb5iPaRrPmK2DIIl-4uSxbFFcnCo9zlCvTLww,1290
7
7
  CasambiBt/_discover.py,sha256=jLc6H69JddrCURgtANZEjws6_UbSzXJtvJkbKTaIUHY,1849
8
8
  CasambiBt/_encryption.py,sha256=CLcoOOrggQqhJbnr_emBnEnkizpWDvb_0yFnitq4_FM,3831
@@ -12,11 +12,11 @@ CasambiBt/_network.py,sha256=ai1o3EybsAhjyPohSOxeE0cWoFvEqdcc3PE3uFDaTfE,21346
12
12
  CasambiBt/_operation.py,sha256=Q5UccsrtNp_B_wWqwH_3eLFW_yF6A55FMmfUKDk2WrI,1059
13
13
  CasambiBt/_switch_events.py,sha256=S8OD0dBcw5T4J2C7qfmOQMnTJ7omIXRUYv4PqDOB87E,13137
14
14
  CasambiBt/_unit.py,sha256=nxbg_8UCCVB9WI8dUS21g2JrGyPKcefqKMSusMOhLOo,18721
15
- CasambiBt/_version.py,sha256=VtWwYBijhWhHtBHLmr-n1eqgiJjNkNATaUweLPXTAo4,337
15
+ CasambiBt/_version.py,sha256=KfDHVZ0HvUoCJCQD90I4l0PCSgOKne4pUVo8Y_Hv5Xk,338
16
16
  CasambiBt/errors.py,sha256=1L_Q8og_N_BRYEKizghAQXr6tihlHykFgtcCHUDcBas,1961
17
17
  CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- casambi_bt_revamped-0.3.12.dev9.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
- casambi_bt_revamped-0.3.12.dev9.dist-info/METADATA,sha256=R31XEvi4eCtF_CYH_DfCnN9lvMmhDS8zCYJlqFAuu2U,5877
20
- casambi_bt_revamped-0.3.12.dev9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
- casambi_bt_revamped-0.3.12.dev9.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
- casambi_bt_revamped-0.3.12.dev9.dist-info/RECORD,,
18
+ casambi_bt_revamped-0.3.12.dev10.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
+ casambi_bt_revamped-0.3.12.dev10.dist-info/METADATA,sha256=VYoGQSXLxJqbL3RSHlXZbuwsMydhxXPvfIPYQnGfhac,5878
20
+ casambi_bt_revamped-0.3.12.dev10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ casambi_bt_revamped-0.3.12.dev10.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
+ casambi_bt_revamped-0.3.12.dev10.dist-info/RECORD,,