casambi-bt-revamped 0.3.7.dev1__py3-none-any.whl → 0.3.7.dev3__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
@@ -423,14 +423,14 @@ class Casambi:
423
423
  f"Handling switch event: unit_id={data.get('unit_id')}, "
424
424
  f"button={data.get('button')}, event={data.get('event')}"
425
425
  )
426
-
426
+
427
427
  # Notify listeners
428
- for h in self._switchEventCallbacks:
428
+ for switch_handler in self._switchEventCallbacks:
429
429
  try:
430
- h(data)
430
+ switch_handler(data)
431
431
  except Exception:
432
432
  self._logger.error(
433
- f"Exception occurred in switchEventCallback {h}.",
433
+ f"Exception occurred in switchEventCallback {switch_handler}.",
434
434
  exc_info=True,
435
435
  )
436
436
  else:
@@ -457,7 +457,9 @@ class Casambi:
457
457
  self._unitChangedCallbacks.remove(handler)
458
458
  self._logger.debug(f"Removed unit changed handler {handler}")
459
459
 
460
- def registerSwitchEventHandler(self, handler: Callable[[dict[str, Any]], None]) -> None:
460
+ def registerSwitchEventHandler(
461
+ self, handler: Callable[[dict[str, Any]], None]
462
+ ) -> None:
461
463
  """Register a new handler for switch events.
462
464
 
463
465
  This handler is called whenever a switch event is received.
@@ -474,7 +476,9 @@ class Casambi:
474
476
  self._switchEventCallbacks.append(handler)
475
477
  self._logger.debug(f"Registered switch event handler {handler}")
476
478
 
477
- def unregisterSwitchEventHandler(self, handler: Callable[[dict[str, Any]], None]) -> None:
479
+ def unregisterSwitchEventHandler(
480
+ self, handler: Callable[[dict[str, Any]], None]
481
+ ) -> None:
478
482
  """Unregister an existing switch event handler.
479
483
 
480
484
  :param handler: The handler to unregister.
CasambiBt/_client.py CHANGED
@@ -33,12 +33,6 @@ 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
-
42
36
 
43
37
  @unique
44
38
  class IncommingPacketType(IntEnum):
@@ -420,28 +414,18 @@ class CasambiClient:
420
414
  ) -> None:
421
415
  # TODO: Check incoming counter and direction flag
422
416
  self._inPacketCount += 1
423
-
424
- # Store raw encrypted packet for Android parser analysis
417
+
418
+ # Store raw encrypted packet for reference
425
419
  raw_encrypted_packet = data[:]
426
-
427
- # Android parser comparison - disabled as protocols are incompatible
428
- android_switch_event = None
429
- # The Android parser expects a completely different packet format than what
430
- # this implementation uses. Logging disabled to reduce noise.
431
420
 
432
421
  try:
433
- decrypted_data = self._encryptor.decryptAndVerify(data, data[:4] + self._nonce[4:])
422
+ decrypted_data = self._encryptor.decryptAndVerify(
423
+ data, data[:4] + self._nonce[4:]
424
+ )
434
425
  except InvalidSignature:
435
426
  # We only drop packets with invalid signature here instead of going into an error state
436
427
  self._logger.error(f"Invalid signature for packet {b2a(data)}!")
437
428
  return
438
-
439
- # Protocol analysis: Current implementation vs Android
440
- # These are fundamentally different protocols:
441
- # - Current: Type 0x07 + message-based protocol (0x10/0x08 messages)
442
- # - Android: Complex header with command types 29-36 for buttons
443
- # No conversion possible - they're incompatible formats
444
- android_switch_event = None
445
429
 
446
430
  packetType = decrypted_data[0]
447
431
  self._logger.debug(f"Incoming data of type {packetType}: {b2a(decrypted_data)}")
@@ -449,7 +433,9 @@ class CasambiClient:
449
433
  if packetType == IncommingPacketType.UnitState:
450
434
  self._parseUnitStates(decrypted_data[1:])
451
435
  elif packetType == IncommingPacketType.SwitchEvent:
452
- self._parseSwitchEvent(decrypted_data[1:], self._inPacketCount, raw_encrypted_packet, android_switch_event)
436
+ self._parseSwitchEvent(
437
+ decrypted_data[1:], self._inPacketCount, raw_encrypted_packet
438
+ )
453
439
  elif packetType == IncommingPacketType.NetworkConfig:
454
440
  # We don't care about the config the network thinks it has.
455
441
  # We assume that cloud config and local config match.
@@ -503,23 +489,29 @@ class CasambiClient:
503
489
  f"Ran out of data while parsing unit state! Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
504
490
  )
505
491
 
506
- def _parseSwitchEvent(self, data: bytes, packet_seq: int = None, raw_packet: bytes = None, android_switch_event: dict = None) -> None:
507
- """Parse switch event packet which contains multiple message types"""
508
- self._logger.info(f"Parsing incoming switch event packet #{packet_seq}... Data: {b2a(data)}")
509
-
492
+ def _parseSwitchEvent(
493
+ self, data: bytes, packet_seq: int = None, raw_packet: bytes = None
494
+ ) -> None:
495
+ """Parse switch event packet which contains multiple message types."""
496
+ self._logger.info(
497
+ f"Parsing incoming switch event packet #{packet_seq}... Data: {b2a(data)}"
498
+ )
499
+
510
500
  # Special handling for message type 0x29 - not a switch event
511
501
  if len(data) >= 1 and data[0] == 0x29:
512
- self._logger.debug(f"Ignoring message type 0x29 (not a switch event): {b2a(data)}")
502
+ self._logger.debug(
503
+ f"Ignoring message type 0x29 (not a switch event): {b2a(data)}"
504
+ )
513
505
  return
514
506
 
515
507
  pos = 0
516
508
  oldPos = 0
517
509
  switch_events_found = 0
518
-
510
+
519
511
  try:
520
512
  while pos <= len(data) - 3:
521
513
  oldPos = pos
522
-
514
+
523
515
  # Parse message header
524
516
  message_type = data[pos]
525
517
  flags = data[pos + 1]
@@ -554,15 +546,19 @@ class CasambiClient:
554
546
  # Extract button ID - try both upper and lower nibbles
555
547
  button_lower = parameter & 0x0F
556
548
  button_upper = (parameter >> 4) & 0x0F
557
-
549
+
558
550
  # Use upper 4 bits if lower 4 bits are 0, otherwise use lower 4 bits
559
551
  if button_lower == 0 and button_upper != 0:
560
552
  button = button_upper
561
- self._logger.debug(f"EVO button extraction: parameter=0x{parameter:02x}, using upper nibble, button={button}")
553
+ self._logger.debug(
554
+ f"EVO button extraction: parameter=0x{parameter:02x}, using upper nibble, button={button}"
555
+ )
562
556
  else:
563
557
  button = button_lower
564
- self._logger.debug(f"EVO button extraction: parameter=0x{parameter:02x}, using lower nibble, button={button}")
565
-
558
+ self._logger.debug(
559
+ f"EVO button extraction: parameter=0x{parameter:02x}, using lower nibble, button={button}"
560
+ )
561
+
566
562
  # For type 0x10 messages, we need to pass additional data beyond the declared payload
567
563
  if message_type == 0x10:
568
564
  # Extend to include at least 10 bytes from message start for state byte
@@ -570,11 +566,20 @@ class CasambiClient:
570
566
  full_message_data = data[oldPos:extended_end]
571
567
  else:
572
568
  full_message_data = data
573
- self._processSwitchMessage(message_type, flags, button, payload, full_message_data, oldPos, packet_seq, raw_packet, android_switch_event)
569
+ self._processSwitchMessage(
570
+ message_type,
571
+ flags,
572
+ button,
573
+ payload,
574
+ full_message_data,
575
+ oldPos,
576
+ packet_seq,
577
+ raw_packet,
578
+ )
574
579
  elif message_type == 0x29:
575
580
  # This shouldn't happen due to check above, but just in case
576
- self._logger.debug(f"Ignoring embedded type 0x29 message")
577
- elif message_type in [0x00, 0x06, 0x09, 0x1f, 0x2a]:
581
+ self._logger.debug("Ignoring embedded type 0x29 message")
582
+ elif message_type in [0x00, 0x06, 0x09, 0x1F, 0x2A]:
578
583
  # Known non-switch message types - log at debug level
579
584
  self._logger.debug(
580
585
  f"Non-switch message type 0x{message_type:02x}: flags=0x{flags:02x}, "
@@ -594,12 +599,22 @@ class CasambiClient:
594
599
  f"Ran out of data while parsing switch event packet! "
595
600
  f"Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
596
601
  )
597
-
602
+
598
603
  if switch_events_found == 0:
599
604
  self._logger.debug(f"No switch events found in packet: {b2a(data)}")
600
605
 
601
- def _processSwitchMessage(self, message_type: int, flags: int, button: int, payload: bytes, full_data: bytes, start_pos: int, packet_seq: int = None, raw_packet: bytes = None, android_switch_event: dict = None) -> None:
602
- """Process a switch/button message (types 0x08 or 0x10)"""
606
+ def _processSwitchMessage(
607
+ self,
608
+ message_type: int,
609
+ flags: int,
610
+ button: int,
611
+ payload: bytes,
612
+ full_data: bytes,
613
+ start_pos: int,
614
+ packet_seq: int = None,
615
+ raw_packet: bytes = None,
616
+ ) -> None:
617
+ """Process a switch/button message (types 0x08 or 0x10)."""
603
618
  if not payload:
604
619
  self._logger.error("Switch message has empty payload")
605
620
  return
@@ -608,14 +623,14 @@ class CasambiClient:
608
623
  if message_type == 0x10 and len(payload) >= 3:
609
624
  # Type 0x10: unit_id is at payload[2]
610
625
  unit_id = payload[2]
611
- extra_data = payload[3:] if len(payload) > 3 else b''
626
+ extra_data = payload[3:] if len(payload) > 3 else b""
612
627
  else:
613
628
  # Standard parsing for other message types
614
629
  unit_id = payload[0]
615
- extra_data = b''
630
+ extra_data = b""
616
631
  if len(payload) > 2:
617
632
  extra_data = payload[2:]
618
-
633
+
619
634
  # Extract action based on message type (action SHOULD be different for press vs release)
620
635
  if message_type == 0x10 and len(payload) > 1:
621
636
  # Type 0x10: action is at payload[1]
@@ -627,7 +642,7 @@ class CasambiClient:
627
642
  action = None
628
643
 
629
644
  event_string = "unknown"
630
-
645
+
631
646
  # Different interpretation based on message type
632
647
  if message_type == 0x08:
633
648
  # Type 0x08: Use bit 1 of action for press/release
@@ -647,10 +662,12 @@ class CasambiClient:
647
662
  event_string = "button_release"
648
663
  elif state_byte == 0x09:
649
664
  event_string = "button_hold"
650
- elif state_byte == 0x0c:
665
+ elif state_byte == 0x0C:
651
666
  event_string = "button_release_after_hold"
652
667
  else:
653
- self._logger.debug(f"Type 0x10: Unknown state byte 0x{state_byte:02x} at message pos {state_pos}")
668
+ self._logger.debug(
669
+ f"Type 0x10: Unknown state byte 0x{state_byte:02x} at message pos {state_pos}"
670
+ )
654
671
  # Fallback: check if extra_data starts with 0x12 (indicates release)
655
672
  if len(extra_data) >= 1 and extra_data[0] == 0x12:
656
673
  event_string = "button_release"
@@ -660,10 +677,14 @@ class CasambiClient:
660
677
  # Fallback when message is too short
661
678
  if len(extra_data) >= 1 and extra_data[0] == 0x12:
662
679
  event_string = "button_release"
663
- self._logger.debug(f"Type 0x10: Using extra_data pattern for release detection")
680
+ self._logger.debug(
681
+ "Type 0x10: Using extra_data pattern for release detection"
682
+ )
664
683
  else:
665
684
  # Cannot determine state
666
- self._logger.warning(f"Type 0x10 message missing state info, unit_id={unit_id}, payload={b2a(payload)}")
685
+ self._logger.warning(
686
+ f"Type 0x10 message missing state info, unit_id={unit_id}, payload={b2a(payload)}"
687
+ )
667
688
  event_string = "unknown"
668
689
 
669
690
  action_display = f"{action:#04x}" if action is not None else "N/A"
@@ -672,29 +693,7 @@ class CasambiClient:
672
693
  f"Switch event (type 0x{message_type:02x}): button={button}, unit_id={unit_id}, "
673
694
  f"action={action_display} ({event_string}), flags=0x{flags:02x}"
674
695
  )
675
-
676
- # Include Android parser comparison if available
677
- android_comparison = None
678
- if android_switch_event:
679
- android_comparison = {
680
- 'unit_id': android_switch_event['unit_id'],
681
- 'button': android_switch_event['button'],
682
- 'state': android_switch_event['state'],
683
- 'param_p': android_switch_event['param_p'],
684
- 'param_s': android_switch_event['param_s'],
685
- 'android_log': android_switch_event['android_log']
686
- }
687
- # Log differences
688
- if android_switch_event['unit_id'] != unit_id:
689
- self._logger.warning(f"Unit ID mismatch: current={unit_id}, android={android_switch_event['unit_id']}")
690
- if android_switch_event['button'] != button:
691
- self._logger.warning(f"Button mismatch: current={button}, android={android_switch_event['button']}")
692
-
693
- # Extract controlling unit if present
694
- controlling_unit = None
695
- # This is redundant since we already return early if unit_id_echo != unit_id
696
- # Removing to avoid confusion
697
-
696
+
698
697
  # Filter out type 0x08 messages with button=0 (likely notifications)
699
698
  if message_type == 0x08 and button == 0:
700
699
  self._logger.debug(
@@ -713,13 +712,11 @@ class CasambiClient:
713
712
  "event": event_string,
714
713
  "flags": flags,
715
714
  "extra_data": extra_data,
716
- "controlling_unit": controlling_unit,
717
715
  "packet_sequence": packet_seq,
718
716
  "raw_packet": b2a(raw_packet) if raw_packet else None,
719
717
  "decrypted_data": b2a(full_data),
720
718
  "message_position": start_pos,
721
719
  "payload_hex": b2a(payload),
722
- "android_comparison": android_comparison,
723
720
  },
724
721
  )
725
722
 
CasambiBt/_unit.py CHANGED
@@ -111,7 +111,6 @@ class UnitState:
111
111
  self._colorsource: ColorSource | None = None
112
112
  self._xy: tuple[float, float] | None = None
113
113
  self._slider: int | None = None
114
- self._onoff: bool | None = None
115
114
 
116
115
  def _check_range(
117
116
  self, value: int | float, min: int | float, max: int | float
@@ -270,20 +269,8 @@ class UnitState:
270
269
  def slider(self) -> None:
271
270
  self.slider = None
272
271
 
273
- @property
274
- def onoff(self) -> bool | None:
275
- return self._onoff
276
-
277
- @onoff.setter
278
- def onoff(self, value: bool) -> None:
279
- self._onoff = value
280
-
281
- @onoff.deleter
282
- def onoff(self) -> None:
283
- self._onoff = None
284
-
285
272
  def __repr__(self) -> str:
286
- return f"UnitState(dimmer={self.dimmer}, vertical={self._vertical}, rgb={self.rgb.__repr__()}, white={self.white}, temperature={self.temperature}, colorsource={self.colorsource}, xy={self.xy}, slider={self.slider}, onoff={self.onoff})"
273
+ return f"UnitState(dimmer={self.dimmer}, vertical={self._vertical}, rgb={self.rgb.__repr__()}, white={self.white}, temperature={self.temperature}, colorsource={self.colorsource}, xy={self.xy}, slider={self.slider})"
287
274
 
288
275
 
289
276
  # TODO: Make unit immutable (refactor state, on, online out of it)
@@ -321,8 +308,6 @@ class Unit:
321
308
  @property
322
309
  def is_on(self) -> bool:
323
310
  """Determine whether the unit is turned on."""
324
- if self.unitType.get_control(UnitControlType.ONOFF) and self._state:
325
- return self._on and self._state.onoff is True
326
311
  if self.unitType.get_control(UnitControlType.DIMMER) and self._state:
327
312
  return (
328
313
  self._on and self._state.dimmer is not None and self._state.dimmer > 0
@@ -400,8 +385,6 @@ class Unit:
400
385
  elif c.type == UnitControlType.SLIDER and state.slider is not None:
401
386
  scale = UnitState.SLIDER_RESOLUTION - c.length
402
387
  scaledValue = state.slider >> scale
403
- elif c.type == UnitControlType.ONOFF and state.onoff is not None:
404
- scaledValue = 1 if state.onoff else 0
405
388
 
406
389
  # Use default if unsupported type or unset value in state
407
390
  else:
@@ -494,8 +477,6 @@ class Unit:
494
477
  elif c.type == UnitControlType.SLIDER:
495
478
  scale = UnitState.SLIDER_RESOLUTION - c.length
496
479
  self._state.slider = cInt << scale
497
- elif c.type == UnitControlType.ONOFF:
498
- self._state.onoff = cInt != 0
499
480
  elif c.type == UnitControlType.UNKOWN:
500
481
  # Might be useful for implementing more state types
501
482
  _LOGGER.debug(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.7.dev1
3
+ Version: 0.3.7.dev3
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
@@ -46,7 +46,7 @@ Have a look at `demo.py` for a small example.
46
46
 
47
47
  ### Switch Event Support
48
48
 
49
- This fork adds support for receiving switch button events:
49
+ This library now supports receiving switch button events:
50
50
 
51
51
  ```python
52
52
  from CasambiBt import Casambi
@@ -1,19 +1,18 @@
1
1
  CasambiBt/__init__.py,sha256=TW445xSu5PV3TyMjJfwaA1JoWvQQ8LXhZgGdDTfWf3s,302
2
2
  CasambiBt/_cache.py,sha256=KZ2xbiHAHXUPa8Gw_75Nw9NL4QSY_sTWHbyYXYUDaB0,3865
3
- CasambiBt/_casambi.py,sha256=gLLkhEcObgapqTx5Mk7WRClyG29UyfZYZCCIhhOg4H4,23101
4
- CasambiBt/_client.py,sha256=smkxC9PDiNa3w5cUTi7n_bVvfpNgw7UWivR-Aw1MIhQ,30573
5
- CasambiBt/_client_android_parser.py,sha256=1lVkVYMO0Nhh9_nkNwgb68hlCS_uD8WxYQDir5hOdHs,7744
3
+ CasambiBt/_casambi.py,sha256=tQgmG-8lHbl4_FDS7NwPrucrqcQZd2kimcJa43TYFaw,23156
4
+ CasambiBt/_client.py,sha256=2kHgkf3ERobTDF47ZMNSFl5WrEkWYgDSXfh6R6PsU0Q,28926
6
5
  CasambiBt/_constants.py,sha256=_AxkG7Btxl4VeS6mO7GJW5Kc9dFs3s9sDmtJ83ZEKNw,359
7
6
  CasambiBt/_discover.py,sha256=H7HpiFYIy9ELvmPXXd_ck-5O5invJf15dDIRk-vO5IE,1696
8
7
  CasambiBt/_encryption.py,sha256=CLcoOOrggQqhJbnr_emBnEnkizpWDvb_0yFnitq4_FM,3831
9
8
  CasambiBt/_keystore.py,sha256=Jdiq0zMPDmhfpheSojKY6sTUpmVrvX_qOyO7yCYd3kw,2788
10
9
  CasambiBt/_network.py,sha256=qcsWn_EsBexzXCv14JcpSIymhuR6Eaf479lZdzpfYBM,14417
11
10
  CasambiBt/_operation.py,sha256=-BuC1Bvtg-G-zSN_b_0JMvXdHZaR6LbTw0S425jg96c,842
12
- CasambiBt/_unit.py,sha256=YiQWoHmMDWHHo4XAjtW8rHsBqIqpmp9MVdv1Mbu2xw4,17043
11
+ CasambiBt/_unit.py,sha256=M-Q8-Xd3qjJSUEvsFtic8E4xDc_gtWYakbTGyoIA-P8,16377
13
12
  CasambiBt/errors.py,sha256=0JgDjaKlAKDes0poWzA8nrTUYQ8qdNfBb8dfaqqzCRA,1664
14
13
  CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- casambi_bt_revamped-0.3.7.dev1.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
16
- casambi_bt_revamped-0.3.7.dev1.dist-info/METADATA,sha256=4OCzm7bTticqnuUd6TsKWgr5POAkvKi3qD-KSvYwXqc,3049
17
- casambi_bt_revamped-0.3.7.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- casambi_bt_revamped-0.3.7.dev1.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
19
- casambi_bt_revamped-0.3.7.dev1.dist-info/RECORD,,
14
+ casambi_bt_revamped-0.3.7.dev3.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
15
+ casambi_bt_revamped-0.3.7.dev3.dist-info/METADATA,sha256=grmzh3IRXluM845lDfwqMJHeP-2QlqslRyOI-wlebAk,3048
16
+ casambi_bt_revamped-0.3.7.dev3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ casambi_bt_revamped-0.3.7.dev3.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
18
+ casambi_bt_revamped-0.3.7.dev3.dist-info/RECORD,,
@@ -1,215 +0,0 @@
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