casambi-bt-revamped 0.3.5__tar.gz → 0.3.6.dev1__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 (24) hide show
  1. {casambi_bt_revamped-0.3.5/src/casambi_bt_revamped.egg-info → casambi_bt_revamped-0.3.6.dev1}/PKG-INFO +1 -1
  2. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/setup.cfg +1 -1
  3. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_client.py +47 -0
  4. casambi_bt_revamped-0.3.6.dev1/src/CasambiBt/_client_android_parser.py +215 -0
  5. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1/src/casambi_bt_revamped.egg-info}/PKG-INFO +1 -1
  6. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/casambi_bt_revamped.egg-info/SOURCES.txt +1 -0
  7. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/LICENSE +0 -0
  8. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/README.md +0 -0
  9. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/pyproject.toml +0 -0
  10. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/__init__.py +0 -0
  11. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_cache.py +0 -0
  12. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_casambi.py +0 -0
  13. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_constants.py +0 -0
  14. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_discover.py +0 -0
  15. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_encryption.py +0 -0
  16. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_keystore.py +0 -0
  17. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_network.py +0 -0
  18. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_operation.py +0 -0
  19. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/_unit.py +0 -0
  20. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/errors.py +0 -0
  21. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/CasambiBt/py.typed +0 -0
  22. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/casambi_bt_revamped.egg-info/dependency_links.txt +0 -0
  23. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
  24. {casambi_bt_revamped-0.3.5 → casambi_bt_revamped-0.3.6.dev1}/src/casambi_bt_revamped.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.5
3
+ Version: 0.3.6.dev1
4
4
  Summary: Enhanced Casambi Bluetooth client library with switch event support
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.5
3
+ version = 0.3.6.dev1
4
4
  author = rankjie
5
5
  author_email = rankjie@gmail.com
6
6
  description = Enhanced Casambi Bluetooth client library with switch event support
@@ -33,6 +33,12 @@ from .errors import ( # noqa: E402
33
33
  UnsupportedProtocolVersion,
34
34
  )
35
35
 
36
+ # Import Android parser for comparison
37
+ try:
38
+ from ._client_android_parser import AndroidPacketParser
39
+ except ImportError:
40
+ AndroidPacketParser = None
41
+
36
42
 
37
43
  @unique
38
44
  class IncommingPacketType(IntEnum):
@@ -488,6 +494,20 @@ class CasambiClient:
488
494
  def _parseSwitchEvent(self, data: bytes, packet_seq: int = None, raw_packet: bytes = None) -> None:
489
495
  """Parse switch event packet which contains multiple message types"""
490
496
  self._logger.info(f"Parsing incoming switch event packet... Data: {b2a(data)}")
497
+
498
+ # Try Android parser for comparison if available
499
+ if AndroidPacketParser and raw_packet:
500
+ try:
501
+ # The raw_packet includes encryption, we need the decrypted version
502
+ # which starts after the type byte (data includes content after type byte)
503
+ # Reconstruct the full decrypted packet for Android parser
504
+ full_packet = bytes([IncommingPacketType.SwitchEvent]) + data
505
+ android_result = AndroidPacketParser.parse_complete_packet(full_packet)
506
+ if android_result.get('switch_event'):
507
+ self._logger.info(f"Android parser result: {android_result['switch_event']['android_log']}")
508
+ self._logger.debug(f"Android parser details: {android_result}")
509
+ except Exception as e:
510
+ self._logger.debug(f"Android parser comparison failed: {e}")
491
511
 
492
512
  pos = 0
493
513
  oldPos = 0
@@ -581,6 +601,32 @@ class CasambiClient:
581
601
  f"Switch event (type 0x{message_type:02x}): button={button}, unit_id={unit_id}, "
582
602
  f"action={action_display} ({event_string}), flags=0x{flags:02x}"
583
603
  )
604
+
605
+ # Also try to parse with Android method if this is the full packet
606
+ android_comparison = None
607
+ if AndroidPacketParser and message_type == 0x07 and len(payload) >= 9:
608
+ try:
609
+ # Try to parse as Android packet structure
610
+ android_parsed = AndroidPacketParser.parse_complete_packet(payload)
611
+ if android_parsed.get('switch_event'):
612
+ android_evt = android_parsed['switch_event']
613
+ android_comparison = {
614
+ 'unit_id': android_evt['unit_id'],
615
+ 'button': android_evt['button'],
616
+ 'state': android_evt['state'],
617
+ 'param_p': android_evt['param_p'],
618
+ 'param_s': android_evt['param_s'],
619
+ 'android_log': android_evt['android_log']
620
+ }
621
+ self._logger.info(f"Android parser comparison: {android_evt['android_log']}")
622
+
623
+ # Log differences
624
+ if android_evt['unit_id'] != unit_id:
625
+ self._logger.warning(f"Unit ID mismatch: current={unit_id}, android={android_evt['unit_id']}")
626
+ if android_evt['button'] != button:
627
+ self._logger.warning(f"Button mismatch: current={button}, android={android_evt['button']}")
628
+ except Exception as e:
629
+ self._logger.debug(f"Android parser comparison failed: {e}")
584
630
 
585
631
  self._dataCallback(
586
632
  IncommingPacketType.SwitchEvent,
@@ -597,6 +643,7 @@ class CasambiClient:
597
643
  "decrypted_data": b2a(full_data),
598
644
  "message_position": start_pos,
599
645
  "payload_hex": b2a(payload),
646
+ "android_comparison": android_comparison,
600
647
  },
601
648
  )
602
649
 
@@ -0,0 +1,215 @@
1
+ """
2
+ Android App Compatible Parser for Casambi Switch Events
3
+
4
+ This module implements the exact parsing logic from the decompiled Android app
5
+ to compare with our current implementation.
6
+ """
7
+
8
+ import struct
9
+ from typing import Dict, Any, Optional, Tuple
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AndroidPacketParser:
16
+ """Parser that follows the exact Android app implementation"""
17
+
18
+ @staticmethod
19
+ def parse_packet_header(data: bytes, pos: int = 0) -> Tuple[Dict[str, Any], int]:
20
+ """
21
+ Parse packet header according to Android app C1775b.Q method (lines 230-243)
22
+
23
+ Returns: (header_dict, new_position)
24
+ """
25
+ if len(data) - pos < 9:
26
+ raise ValueError("Insufficient data for packet header")
27
+
28
+ # Read header (2 bytes)
29
+ unsigned_short = struct.unpack_from('>H', data, pos)[0]
30
+ pos += 2
31
+
32
+ # Extract flags from header
33
+ has_origin_handle = (unsigned_short & 64) != 0 # bit 6
34
+ is_unique = (unsigned_short & 128) != 0 # bit 7
35
+ has_session = (unsigned_short & 256) != 0 # bit 8
36
+ has_origin_handle_alt = (unsigned_short & 512) != 0 # bit 9
37
+ flag_1024 = (unsigned_short & 1024) != 0 # bit 10
38
+
39
+ # Read command type (1 byte) - this is the EnumC1777d ordinal
40
+ command_type = data[pos] & 0xFF
41
+ pos += 1
42
+
43
+ # Read origin (2 bytes)
44
+ origin = struct.unpack_from('>H', data, pos)[0]
45
+ pos += 2
46
+
47
+ # Read target (2 bytes)
48
+ target = struct.unpack_from('>H', data, pos)[0]
49
+ pos += 2
50
+
51
+ # Extract lifetime from header bits 11-14
52
+ lifetime = (unsigned_short >> 11) & 15
53
+
54
+ # Read age (2 bytes)
55
+ age = struct.unpack_from('>H', data, pos)[0]
56
+ pos += 2
57
+
58
+ # Read optional origin handle if flag is set
59
+ origin_handle = None
60
+ if has_origin_handle_alt:
61
+ origin_handle = data[pos] & 0xFF
62
+ pos += 1
63
+
64
+ # Extract payload length from header bits 0-5
65
+ payload_length = unsigned_short & 63
66
+
67
+ header = {
68
+ 'header_raw': unsigned_short,
69
+ 'command_type': command_type,
70
+ 'origin': origin,
71
+ 'target': target,
72
+ 'lifetime': lifetime,
73
+ 'age': age,
74
+ 'origin_handle': origin_handle,
75
+ 'payload_length': payload_length,
76
+ 'flags': {
77
+ 'has_origin_handle': has_origin_handle,
78
+ 'is_unique': is_unique,
79
+ 'has_session': has_session,
80
+ 'has_origin_handle_alt': has_origin_handle_alt,
81
+ 'flag_1024': flag_1024
82
+ }
83
+ }
84
+
85
+ return header, pos
86
+
87
+ @staticmethod
88
+ def parse_switch_event(header: Dict[str, Any], payload: bytes) -> Optional[Dict[str, Any]]:
89
+ """
90
+ Parse switch event according to Android app logic (lines 271-280)
91
+
92
+ The Android app checks:
93
+ - target & 0xFF == 6 (lower byte of target must be 6)
94
+ - command_type ordinal must be between 29-36 (FunctionButtonEvent0-7)
95
+ - payload must have at least 3 bytes
96
+ """
97
+ target_type = header['target'] & 0xFF
98
+ target_unit_id = header['target'] >> 8
99
+ command_type = header['command_type']
100
+
101
+ # Check if this is a switch event
102
+ if target_type != 6:
103
+ return None
104
+
105
+ # Check if command type is in range for button events (29-36)
106
+ if command_type < 29 or command_type > 36:
107
+ return None
108
+
109
+ # Check payload length
110
+ if len(payload) < 3:
111
+ logger.warning(f"Switch event payload too short: {len(payload)} bytes")
112
+ return None
113
+
114
+ # Parse according to Android logic
115
+ first_byte = payload[0]
116
+ button_index = command_type - 29 # 0-7
117
+
118
+ # Extract parameters from first byte
119
+ param_p = (first_byte >> 3) & 15 # bits 3-6
120
+ param_s = first_byte & 7 # bits 0-2
121
+ state = 1 if (first_byte & 128) else 0 # bit 7
122
+
123
+ return {
124
+ 'unit_id': target_unit_id,
125
+ 'button': button_index,
126
+ 'state': state, # 1 = pressed, 0 = released
127
+ 'param_p': param_p,
128
+ 'param_s': param_s,
129
+ 'target_type': target_type,
130
+ 'command_type': command_type,
131
+ 'payload_hex': payload.hex(),
132
+ 'android_log': f"Unit {target_unit_id} Switch event: #{button_index} (P{param_p} S{param_s}) = {state}"
133
+ }
134
+
135
+ @staticmethod
136
+ def parse_complete_packet(data: bytes) -> Dict[str, Any]:
137
+ """Parse a complete packet and extract switch events if present"""
138
+ try:
139
+ header, payload_start = AndroidPacketParser.parse_packet_header(data)
140
+
141
+ # Read payload
142
+ payload_length = header['payload_length']
143
+ if len(data) - payload_start < payload_length:
144
+ raise ValueError(f"Insufficient data for payload: need {payload_length}, have {len(data) - payload_start}")
145
+
146
+ payload = data[payload_start:payload_start + payload_length]
147
+
148
+ # Try to parse as switch event
149
+ switch_event = AndroidPacketParser.parse_switch_event(header, payload)
150
+
151
+ return {
152
+ 'header': header,
153
+ 'payload': payload.hex(),
154
+ 'switch_event': switch_event
155
+ }
156
+
157
+ except Exception as e:
158
+ logger.error(f"Failed to parse packet: {e}")
159
+ return {'error': str(e), 'data': data.hex()}
160
+
161
+
162
+ # Command type constants from EnumC1777d
163
+ class FunctionType:
164
+ """Function type constants from Android app"""
165
+ BUTTON_EVENT_0 = 29
166
+ BUTTON_EVENT_1 = 30
167
+ BUTTON_EVENT_2 = 31
168
+ BUTTON_EVENT_3 = 32
169
+ BUTTON_EVENT_4 = 33
170
+ BUTTON_EVENT_5 = 34
171
+ BUTTON_EVENT_6 = 35
172
+ BUTTON_EVENT_7 = 36
173
+
174
+
175
+ def compare_with_current_parser(data: bytes, current_parser_result: Dict[str, Any]) -> Dict[str, Any]:
176
+ """Compare Android parser results with current implementation"""
177
+ android_result = AndroidPacketParser.parse_complete_packet(data)
178
+
179
+ comparison = {
180
+ 'android_parser': android_result,
181
+ 'current_parser': current_parser_result,
182
+ 'differences': []
183
+ }
184
+
185
+ # Compare results
186
+ if android_result.get('switch_event') and current_parser_result:
187
+ android_evt = android_result['switch_event']
188
+
189
+ # Check unit_id
190
+ if android_evt['unit_id'] != current_parser_result.get('unit_id'):
191
+ comparison['differences'].append({
192
+ 'field': 'unit_id',
193
+ 'android': android_evt['unit_id'],
194
+ 'current': current_parser_result.get('unit_id')
195
+ })
196
+
197
+ # Check button
198
+ if android_evt['button'] != current_parser_result.get('button'):
199
+ comparison['differences'].append({
200
+ 'field': 'button',
201
+ 'android': android_evt['button'],
202
+ 'current': current_parser_result.get('button')
203
+ })
204
+
205
+ # Map Android state to current event names
206
+ android_event = 'button_press' if android_evt['state'] == 1 else 'button_release'
207
+ if android_event != current_parser_result.get('event'):
208
+ comparison['differences'].append({
209
+ 'field': 'event',
210
+ 'android': android_event,
211
+ 'current': current_parser_result.get('event'),
212
+ 'note': 'Android only has press/release, no hold detection'
213
+ })
214
+
215
+ return comparison
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.5
3
+ Version: 0.3.6.dev1
4
4
  Summary: Enhanced Casambi Bluetooth client library with switch event support
5
5
  Home-page: https://github.com/rankjie/casambi-bt
6
6
  Author: rankjie
@@ -6,6 +6,7 @@ src/CasambiBt/__init__.py
6
6
  src/CasambiBt/_cache.py
7
7
  src/CasambiBt/_casambi.py
8
8
  src/CasambiBt/_client.py
9
+ src/CasambiBt/_client_android_parser.py
9
10
  src/CasambiBt/_constants.py
10
11
  src/CasambiBt/_discover.py
11
12
  src/CasambiBt/_encryption.py