casambi-bt-revamped 0.3.12.dev10__py3-none-any.whl → 0.3.12.dev11__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 +6 -1
- CasambiBt/_client.py +220 -1
- {casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.dist-info}/METADATA +1 -1
- {casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.dist-info}/RECORD +7 -7
- {casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.dist-info}/top_level.txt +0 -0
CasambiBt/_casambi.py
CHANGED
|
@@ -169,10 +169,15 @@ class Casambi:
|
|
|
169
169
|
self._casaClient = cast(CasambiClient, self._casaClient)
|
|
170
170
|
await self._casaClient.connect()
|
|
171
171
|
try:
|
|
172
|
-
# EVO requires key exchange + authenticate; Classic is ready after `connect()`.
|
|
173
172
|
if self._casaClient.protocolMode == ProtocolMode.EVO:
|
|
173
|
+
# EVO requires key exchange + authenticate.
|
|
174
174
|
await self._casaClient.exchangeKey()
|
|
175
175
|
await self._casaClient.authenticate()
|
|
176
|
+
elif self._casaClient.protocolMode == ProtocolMode.CLASSIC:
|
|
177
|
+
# Classic needs an init write to trigger state broadcasts.
|
|
178
|
+
# In EVO the key exchange/auth handshake implicitly signals the
|
|
179
|
+
# device; Classic has no such handshake so we send a time-sync.
|
|
180
|
+
await self._casaClient.classicSendInit()
|
|
176
181
|
except ProtocolError as e:
|
|
177
182
|
await self._casaClient.disconnect()
|
|
178
183
|
raise e
|
CasambiBt/_client.py
CHANGED
|
@@ -159,6 +159,7 @@ class CasambiClient:
|
|
|
159
159
|
self._classicRxType9 = 0
|
|
160
160
|
self._classicRxCmdStream = 0
|
|
161
161
|
self._classicRxUnknown = 0
|
|
162
|
+
self._classicRxClassicStates = 0
|
|
162
163
|
# Per-kind sample counters to ensure we emit at least a few examples for reverse engineering.
|
|
163
164
|
self._classicRxKindSamples: dict[str, int] = {}
|
|
164
165
|
self._classicRxLastStatsTs = time.monotonic()
|
|
@@ -1222,6 +1223,75 @@ class CasambiClient:
|
|
|
1222
1223
|
len(pkt),
|
|
1223
1224
|
)
|
|
1224
1225
|
|
|
1226
|
+
async def classicSendInit(self) -> None:
|
|
1227
|
+
"""Send Classic post-connection initialization (time-sync).
|
|
1228
|
+
|
|
1229
|
+
Ground truth: casambi-android AbstractC1717h.X() (lines 254-345).
|
|
1230
|
+
The Android app sends this as the first packet after Classic connection.
|
|
1231
|
+
In EVO, the key exchange/auth handshake implicitly signals the device;
|
|
1232
|
+
Classic has no such handshake, so an explicit init write is needed to
|
|
1233
|
+
trigger the device to start broadcasting state notifications.
|
|
1234
|
+
|
|
1235
|
+
The payload is sent raw via _sendClassic (NOT wrapped in buildClassicCommand).
|
|
1236
|
+
"""
|
|
1237
|
+
self._checkState(ConnectionState.AUTHENTICATED)
|
|
1238
|
+
if self._protocolMode != ProtocolMode.CLASSIC:
|
|
1239
|
+
return
|
|
1240
|
+
|
|
1241
|
+
import datetime as _dt
|
|
1242
|
+
|
|
1243
|
+
now = _dt.datetime.now()
|
|
1244
|
+
|
|
1245
|
+
# Timezone offset in minutes from UTC.
|
|
1246
|
+
local_tz = _dt.datetime.now(_dt.timezone.utc).astimezone().tzinfo
|
|
1247
|
+
utc_offset_minutes = 0
|
|
1248
|
+
if local_tz is not None:
|
|
1249
|
+
offset = local_tz.utcoffset(now)
|
|
1250
|
+
if offset is not None:
|
|
1251
|
+
utc_offset_minutes = int(offset.total_seconds()) // 60
|
|
1252
|
+
|
|
1253
|
+
# Build the time-sync payload.
|
|
1254
|
+
# Format: [10][year:2BE][month:1][day:1][hour:1][min:1][sec:1]
|
|
1255
|
+
# [tz_offset:2BE signed][dst_transition:4BE][dst_change:1]
|
|
1256
|
+
# [timestamp1:4BE][timestamp2:4BE][zero:2][millis:3BE]
|
|
1257
|
+
payload = bytearray()
|
|
1258
|
+
payload.append(10) # Classic time-sync command byte
|
|
1259
|
+
payload.extend(struct.pack(">H", now.year))
|
|
1260
|
+
payload.append(now.month)
|
|
1261
|
+
payload.append(now.day)
|
|
1262
|
+
payload.append(now.hour)
|
|
1263
|
+
payload.append(now.minute)
|
|
1264
|
+
payload.append(now.second)
|
|
1265
|
+
payload.extend(struct.pack(">h", utc_offset_minutes))
|
|
1266
|
+
# DST transition data and change minutes (0 = no DST info).
|
|
1267
|
+
payload.extend(struct.pack(">I", 0))
|
|
1268
|
+
payload.append(0)
|
|
1269
|
+
# Classic extra bytes: timestamps, zero short, millis.
|
|
1270
|
+
# Start with zeros - refine after tester feedback if needed.
|
|
1271
|
+
payload.extend(struct.pack(">I", 0)) # timestamp1
|
|
1272
|
+
payload.extend(struct.pack(">I", 0)) # timestamp2
|
|
1273
|
+
payload.extend(struct.pack(">H", 0)) # zero
|
|
1274
|
+
# j() in Android is a 3-byte big-endian write.
|
|
1275
|
+
millis_val = now.microsecond // 1000 * 1000
|
|
1276
|
+
payload.append((millis_val >> 16) & 0xFF)
|
|
1277
|
+
payload.append((millis_val >> 8) & 0xFF)
|
|
1278
|
+
payload.append(millis_val & 0xFF)
|
|
1279
|
+
|
|
1280
|
+
self._logger.warning(
|
|
1281
|
+
"[CASAMBI_CLASSIC_INIT] sending time-sync len=%d hex=%s",
|
|
1282
|
+
len(payload),
|
|
1283
|
+
b2a(bytes(payload)),
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
try:
|
|
1287
|
+
await self._sendClassic(bytes(payload))
|
|
1288
|
+
self._logger.warning("[CASAMBI_CLASSIC_INIT] time-sync sent successfully")
|
|
1289
|
+
except Exception:
|
|
1290
|
+
self._logger.warning(
|
|
1291
|
+
"[CASAMBI_CLASSIC_INIT] time-sync send failed",
|
|
1292
|
+
exc_info=True,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1225
1295
|
def _establishedNofityCallback(
|
|
1226
1296
|
self, handle: BleakGATTCharacteristic, data: bytes
|
|
1227
1297
|
) -> None:
|
|
@@ -1579,7 +1649,7 @@ class CasambiClient:
|
|
|
1579
1649
|
self._classicRxLastStatsTs = now
|
|
1580
1650
|
self._logger.warning(
|
|
1581
1651
|
"[CASAMBI_CLASSIC_RX_STATS] frames=%d verified=%d unverifiable=%d parse_fail=%d header=%s "
|
|
1582
|
-
"type6=%d type7=%d type9=%d cmdstream=%d unknown=%d",
|
|
1652
|
+
"type6=%d type7=%d type9=%d cmdstream=%d unknown=%d classic_states=%d",
|
|
1583
1653
|
self._classicRxFrames,
|
|
1584
1654
|
self._classicRxVerified,
|
|
1585
1655
|
self._classicRxUnverifiable,
|
|
@@ -1590,8 +1660,17 @@ class CasambiClient:
|
|
|
1590
1660
|
self._classicRxType9,
|
|
1591
1661
|
self._classicRxCmdStream,
|
|
1592
1662
|
self._classicRxUnknown,
|
|
1663
|
+
self._classicRxClassicStates,
|
|
1593
1664
|
)
|
|
1594
1665
|
|
|
1666
|
+
# Classic payloads use a completely different format from EVO.
|
|
1667
|
+
# Classic: byte 0 is a type indicator (0=netconfig, 255=log, else=unit_id).
|
|
1668
|
+
# EVO: byte 0 is a packet type (6=UnitState, 7=Switch, 9=NetConfig).
|
|
1669
|
+
# Dispatch Classic through its own parser to avoid misinterpretation.
|
|
1670
|
+
if self._protocolMode == ProtocolMode.CLASSIC:
|
|
1671
|
+
self._dispatchClassicPayload(payload)
|
|
1672
|
+
return
|
|
1673
|
+
|
|
1595
1674
|
# If the payload starts with a known EVO packet type, reuse existing parsers.
|
|
1596
1675
|
packet_type = payload[0]
|
|
1597
1676
|
if packet_type in (IncommingPacketType.UnitState, IncommingPacketType.SwitchEvent, IncommingPacketType.NetworkConfig):
|
|
@@ -1702,6 +1781,146 @@ class CasambiClient:
|
|
|
1702
1781
|
b2a(payload[pos:]),
|
|
1703
1782
|
)
|
|
1704
1783
|
|
|
1784
|
+
def _dispatchClassicPayload(self, payload: bytes) -> None:
|
|
1785
|
+
"""Dispatch a verified Classic payload based on its type indicator.
|
|
1786
|
+
|
|
1787
|
+
Classic payloads (from C1751c.V()) use a different format from EVO:
|
|
1788
|
+
- byte 0 == 0: network config data
|
|
1789
|
+
- byte 0 == 255: log message
|
|
1790
|
+
- otherwise: unit state stream (byte 0 is the first unit_id)
|
|
1791
|
+
"""
|
|
1792
|
+
if not payload:
|
|
1793
|
+
return
|
|
1794
|
+
|
|
1795
|
+
first_byte = payload[0]
|
|
1796
|
+
|
|
1797
|
+
# Log full payload for the first 10 Classic payloads regardless of type.
|
|
1798
|
+
if self._classicRxClassicStates < 10:
|
|
1799
|
+
self._logger.warning(
|
|
1800
|
+
"[CASAMBI_CLASSIC_DISPATCH] #%d type_byte=%d len=%d hex=%s",
|
|
1801
|
+
self._classicRxClassicStates,
|
|
1802
|
+
first_byte,
|
|
1803
|
+
len(payload),
|
|
1804
|
+
b2a(payload[: min(len(payload), 64)]).decode("ascii")
|
|
1805
|
+
+ ("..." if len(payload) > 64 else ""),
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
if first_byte == 0:
|
|
1809
|
+
self._logger.debug("[CASAMBI_CLASSIC_NETCONFIG] len=%d", len(payload))
|
|
1810
|
+
return
|
|
1811
|
+
|
|
1812
|
+
if first_byte == 255:
|
|
1813
|
+
self._logger.debug("[CASAMBI_CLASSIC_LOG] len=%d", len(payload))
|
|
1814
|
+
return
|
|
1815
|
+
|
|
1816
|
+
# Unit state stream: entire payload is passed (first byte is the first unit_id).
|
|
1817
|
+
self._classicRxClassicStates += 1
|
|
1818
|
+
self._parseClassicUnitStates(payload)
|
|
1819
|
+
|
|
1820
|
+
def _parseClassicUnitStates(self, data: bytes) -> None:
|
|
1821
|
+
"""Parse Classic unit state records.
|
|
1822
|
+
|
|
1823
|
+
Ground truth: casambi-android C1751c.V() (line 301+).
|
|
1824
|
+
Format is completely different from EVO _parseUnitStates:
|
|
1825
|
+
- flags lower nibble = state_len (EVO uses a separate byte)
|
|
1826
|
+
- flags bit 5 = extra1 present, bit 6 = extra2 present, bit 7 = offline
|
|
1827
|
+
- unit_id 0xF0 = command response (skip)
|
|
1828
|
+
"""
|
|
1829
|
+
self._logger.debug("Parsing Classic unit states...")
|
|
1830
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
1831
|
+
self._logger.debug("[CASAMBI_CLASSIC_STATES_RAW] len=%d hex=%s", len(data), b2a(data))
|
|
1832
|
+
|
|
1833
|
+
pos = 0
|
|
1834
|
+
old_pos = 0
|
|
1835
|
+
records_parsed = 0
|
|
1836
|
+
try:
|
|
1837
|
+
while pos + 2 <= len(data):
|
|
1838
|
+
unit_id = data[pos]
|
|
1839
|
+
flags = data[pos + 1]
|
|
1840
|
+
pos += 2
|
|
1841
|
+
|
|
1842
|
+
state_len = flags & 0x0F
|
|
1843
|
+
has_extra1 = (flags & 0x20) != 0
|
|
1844
|
+
has_extra2 = (flags & 0x40) != 0
|
|
1845
|
+
is_offline = (flags & 0x80) != 0
|
|
1846
|
+
|
|
1847
|
+
# 0xF0 = command response record, skip state_len bytes.
|
|
1848
|
+
if unit_id == 0xF0:
|
|
1849
|
+
pos += state_len
|
|
1850
|
+
continue
|
|
1851
|
+
|
|
1852
|
+
extra1 = 0
|
|
1853
|
+
if has_extra1:
|
|
1854
|
+
if pos >= len(data):
|
|
1855
|
+
break
|
|
1856
|
+
extra1 = data[pos]
|
|
1857
|
+
pos += 1
|
|
1858
|
+
|
|
1859
|
+
extra2 = 0
|
|
1860
|
+
if has_extra2:
|
|
1861
|
+
if pos >= len(data):
|
|
1862
|
+
break
|
|
1863
|
+
extra2 = data[pos]
|
|
1864
|
+
pos += 1
|
|
1865
|
+
|
|
1866
|
+
if pos + state_len > len(data):
|
|
1867
|
+
break
|
|
1868
|
+
|
|
1869
|
+
state = data[pos : pos + state_len]
|
|
1870
|
+
pos += state_len
|
|
1871
|
+
records_parsed += 1
|
|
1872
|
+
|
|
1873
|
+
# Log the first few parsed records at WARNING level for tester visibility.
|
|
1874
|
+
if records_parsed <= 10 or self._logger.isEnabledFor(logging.DEBUG):
|
|
1875
|
+
self._logger.warning(
|
|
1876
|
+
"[CASAMBI_CLASSIC_STATE_PARSED] unit=%d flags=0x%02x state_len=%d "
|
|
1877
|
+
"offline=%s extra1=%d extra2=%d state=%s",
|
|
1878
|
+
unit_id,
|
|
1879
|
+
flags,
|
|
1880
|
+
state_len,
|
|
1881
|
+
is_offline,
|
|
1882
|
+
extra1,
|
|
1883
|
+
extra2,
|
|
1884
|
+
b2a(state),
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
online = not is_offline
|
|
1888
|
+
# Let Unit.is_on derive actual on/off from state bytes (dimmer, onoff).
|
|
1889
|
+
on = True
|
|
1890
|
+
|
|
1891
|
+
self._dataCallback(
|
|
1892
|
+
IncommingPacketType.UnitState,
|
|
1893
|
+
{
|
|
1894
|
+
"id": unit_id,
|
|
1895
|
+
"online": online,
|
|
1896
|
+
"on": on,
|
|
1897
|
+
"state": state,
|
|
1898
|
+
"flags": flags,
|
|
1899
|
+
"prio": 0,
|
|
1900
|
+
"state_len": state_len,
|
|
1901
|
+
"padding_len": 0,
|
|
1902
|
+
"con": None,
|
|
1903
|
+
"sid": None,
|
|
1904
|
+
"extra_byte": extra1,
|
|
1905
|
+
"extra_float": extra1 / 255.0 if extra1 else 0.0,
|
|
1906
|
+
},
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
old_pos = pos
|
|
1910
|
+
except IndexError:
|
|
1911
|
+
self._logger.error(
|
|
1912
|
+
"Ran out of data while parsing Classic unit state! Remaining data %s in %s.",
|
|
1913
|
+
b2a(data[old_pos:]),
|
|
1914
|
+
b2a(data),
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
if records_parsed > 0:
|
|
1918
|
+
self._logger.debug(
|
|
1919
|
+
"[CASAMBI_CLASSIC_STATES_DONE] records=%d remaining=%d",
|
|
1920
|
+
records_parsed,
|
|
1921
|
+
len(data) - pos,
|
|
1922
|
+
)
|
|
1923
|
+
|
|
1705
1924
|
def _parseUnitStates(self, data: bytes) -> None:
|
|
1706
1925
|
# Ground truth: casambi-android `v1.C1775b.V(Q2.h)` parses decrypted packet type=6
|
|
1707
1926
|
# as a stream of unit state records. Records have optional bytes depending on flags.
|
{casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.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.dev11
|
|
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.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.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=
|
|
3
|
+
CasambiBt/_casambi.py,sha256=dAZZ0S2-t2ShLbW78AE9lOLBzOmhBOTTXJky-6khdkE,41981
|
|
4
4
|
CasambiBt/_classic_crypto.py,sha256=XIp3JBaeY8hIUv5kB0ygVG_eRx9AgHHF4ts2--CFm78,4973
|
|
5
|
-
CasambiBt/_client.py,sha256
|
|
5
|
+
CasambiBt/_client.py,sha256=-C1WfIa8IOTB5R9-nGSKJwE6yLtJC5Meo5FKUq9_XEw,92546
|
|
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
|
|
@@ -15,8 +15,8 @@ CasambiBt/_unit.py,sha256=nxbg_8UCCVB9WI8dUS21g2JrGyPKcefqKMSusMOhLOo,18721
|
|
|
15
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.dev11.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
19
|
+
casambi_bt_revamped-0.3.12.dev11.dist-info/METADATA,sha256=fTNunmvIZDbzGByz_OJPXkDf2Jstu7y2Rp5DCxZ62IY,5878
|
|
20
|
+
casambi_bt_revamped-0.3.12.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
casambi_bt_revamped-0.3.12.dev11.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
22
|
+
casambi_bt_revamped-0.3.12.dev11.dist-info/RECORD,,
|
{casambi_bt_revamped-0.3.12.dev10.dist-info → casambi_bt_revamped-0.3.12.dev11.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|