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 +26 -9
- CasambiBt/_classic_crypto.py +123 -8
- CasambiBt/_client.py +161 -2
- CasambiBt/_version.py +1 -1
- {casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/METADATA +1 -1
- {casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/RECORD +9 -9
- {casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
CasambiBt/_classic_crypto.py
CHANGED
|
@@ -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(
|
|
21
|
-
cmac.
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
{casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.12.
|
|
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
|
{casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/RECORD
RENAMED
|
@@ -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=
|
|
4
|
-
CasambiBt/_classic_crypto.py,sha256=
|
|
5
|
-
CasambiBt/_client.py,sha256=
|
|
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=
|
|
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.
|
|
19
|
-
casambi_bt_revamped-0.3.12.
|
|
20
|
-
casambi_bt_revamped-0.3.12.
|
|
21
|
-
casambi_bt_revamped-0.3.12.
|
|
22
|
-
casambi_bt_revamped-0.3.12.
|
|
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,,
|
{casambi_bt_revamped-0.3.12.dev9.dist-info → casambi_bt_revamped-0.3.12.dev10.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|