casambi-bt-revamped 0.3.7.dev5__py3-none-any.whl → 0.3.7.dev6__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
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import struct
4
- from binascii import b2a_hex as b2a
4
+ from binascii import b2a_hex as b2a, hexlify
5
5
  from collections.abc import Callable
6
6
  from enum import IntEnum, unique
7
7
  from hashlib import sha256
@@ -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,36 +414,34 @@ 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
420
 
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.
421
+ # Log raw encrypted packet with special marker for easy filtering
422
+ self._logger.info(f"[CASAMBI_RAW_PACKET] Encrypted #{self._inPacketCount}: {b2a(raw_encrypted_packet)}")
431
423
 
432
424
  try:
433
- decrypted_data = self._encryptor.decryptAndVerify(data, data[:4] + self._nonce[4:])
425
+ decrypted_data = self._encryptor.decryptAndVerify(
426
+ data, data[:4] + self._nonce[4:]
427
+ )
434
428
  except InvalidSignature:
435
429
  # We only drop packets with invalid signature here instead of going into an error state
436
430
  self._logger.error(f"Invalid signature for packet {b2a(data)}!")
437
431
  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
432
 
446
433
  packetType = decrypted_data[0]
447
434
  self._logger.debug(f"Incoming data of type {packetType}: {b2a(decrypted_data)}")
435
+
436
+ # Log decrypted packet with special marker
437
+ self._logger.info(f"[CASAMBI_DECRYPTED] Type={packetType} #{self._inPacketCount}: {b2a(decrypted_data)}")
448
438
 
449
439
  if packetType == IncommingPacketType.UnitState:
450
440
  self._parseUnitStates(decrypted_data[1:])
451
441
  elif packetType == IncommingPacketType.SwitchEvent:
452
- self._parseSwitchEvent(decrypted_data[1:], self._inPacketCount, raw_encrypted_packet, android_switch_event)
442
+ self._parseSwitchEvent(
443
+ decrypted_data[1:], self._inPacketCount, raw_encrypted_packet
444
+ )
453
445
  elif packetType == IncommingPacketType.NetworkConfig:
454
446
  # We don't care about the config the network thinks it has.
455
447
  # We assume that cloud config and local config match.
@@ -503,32 +495,45 @@ class CasambiClient:
503
495
  f"Ran out of data while parsing unit state! Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
504
496
  )
505
497
 
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)}")
498
+ def _parseSwitchEvent(
499
+ self, data: bytes, packet_seq: int = None, raw_packet: bytes = None
500
+ ) -> None:
501
+ """Parse switch event packet which contains multiple message types."""
502
+ self._logger.info(
503
+ f"Parsing incoming switch event packet #{packet_seq}... Data: {b2a(data)}"
504
+ )
509
505
 
506
+ # Log complete packet structure with marker
507
+ self._logger.info(f"[CASAMBI_SWITCH_PACKET] Full data #{packet_seq}: hex={b2a(data)} len={len(data)}")
508
+
510
509
  # Special handling for message type 0x29 - not a switch event
511
510
  if len(data) >= 1 and data[0] == 0x29:
512
- self._logger.debug(f"Ignoring message type 0x29 (not a switch event): {b2a(data)}")
511
+ self._logger.debug(
512
+ f"Ignoring message type 0x29 (not a switch event): {b2a(data)}"
513
+ )
513
514
  return
514
515
 
515
- # NEW PARSING: Android-style protocol parsing (for comparison only)
516
- self._parseAndroidProtocol(data, packet_seq, raw_packet)
517
-
518
516
  pos = 0
519
517
  oldPos = 0
520
518
  switch_events_found = 0
521
-
519
+ all_messages_found = []
520
+
522
521
  try:
523
522
  while pos <= len(data) - 3:
524
523
  oldPos = pos
525
-
524
+
526
525
  # Parse message header
527
526
  message_type = data[pos]
528
527
  flags = data[pos + 1]
529
528
  length = ((data[pos + 2] >> 4) & 15) + 1
530
529
  parameter = data[pos + 2] # Full byte, not just lower 4 bits
531
530
  pos += 3
531
+
532
+ # Log every message found with detailed structure
533
+ self._logger.info(
534
+ f"[CASAMBI_MSG_FOUND] At pos={oldPos}: type=0x{message_type:02x} flags=0x{flags:02x} "
535
+ f"len={length} param=0x{parameter:02x}"
536
+ )
532
537
 
533
538
  # Sanity check: message type should be reasonable
534
539
  if message_type > 0x80:
@@ -550,45 +555,41 @@ class CasambiClient:
550
555
  # Extract the payload
551
556
  payload = data[pos : pos + length]
552
557
  pos += length
558
+
559
+ # Log the payload
560
+ self._logger.info(
561
+ f"[CASAMBI_MSG_PAYLOAD] Type 0x{message_type:02x} payload: {b2a(payload)} "
562
+ f"(bytes {oldPos+3} to {oldPos+3+length-1})"
563
+ )
564
+
565
+ # Track all messages
566
+ all_messages_found.append({
567
+ 'type': message_type,
568
+ 'pos': oldPos,
569
+ 'flags': flags,
570
+ 'param': parameter,
571
+ 'payload': b2a(payload)
572
+ })
553
573
 
554
574
  # Process based on message type
555
575
  if message_type == 0x08 or message_type == 0x10: # Switch/button events
556
576
  switch_events_found += 1
557
-
558
- # Enhanced button extraction for battery-powered switches
559
- # For type 0x08, the parameter byte encodes the button differently
560
- if message_type == 0x08:
561
- # Type 0x08: Check if this might be using Android-style encoding
562
- # where button is in upper nibble when lower nibble is 0
563
- button_lower = parameter & 0x0F
564
- button_upper = (parameter >> 4) & 0x0F
565
-
566
- # Log both interpretations for comparison
577
+ # Extract button ID - try both upper and lower nibbles
578
+ button_lower = parameter & 0x0F
579
+ button_upper = (parameter >> 4) & 0x0F
580
+
581
+ # Use upper 4 bits if lower 4 bits are 0, otherwise use lower 4 bits
582
+ if button_lower == 0 and button_upper != 0:
583
+ button = button_upper
567
584
  self._logger.debug(
568
- f"[TYPE 0x08 ANALYSIS] parameter=0x{parameter:02x}, "
569
- f"lower_nibble={button_lower}, upper_nibble={button_upper}"
585
+ f"EVO button extraction: parameter=0x{parameter:02x}, using upper nibble, button={button}"
570
586
  )
571
-
572
- # If lower nibble is 0 and upper nibble is not, use upper
573
- # This handles cases like 0x20 (button 2), 0x30 (button 3), etc.
574
- if button_lower == 0 and button_upper != 0:
575
- button = button_upper
576
- self._logger.info(
577
- f"[TYPE 0x08 FIX] Using upper nibble for button: {button} "
578
- f"(parameter=0x{parameter:02x})"
579
- )
580
- else:
581
- button = button_lower
582
- self._logger.debug(
583
- f"[TYPE 0x08] Using lower nibble for button: {button} "
584
- f"(parameter=0x{parameter:02x})"
585
- )
586
587
  else:
587
- # Type 0x10: Standard extraction from lower nibble
588
- button = parameter & 0x0F
588
+ button = button_lower
589
589
  self._logger.debug(
590
- f"[TYPE 0x10] Button extraction: parameter=0x{parameter:02x}, button={button}"
590
+ f"EVO button extraction: parameter=0x{parameter:02x}, using lower nibble, button={button}"
591
591
  )
592
+
592
593
  # For type 0x10 messages, we need to pass additional data beyond the declared payload
593
594
  if message_type == 0x10:
594
595
  # Extend to include at least 10 bytes from message start for state byte
@@ -596,11 +597,20 @@ class CasambiClient:
596
597
  full_message_data = data[oldPos:extended_end]
597
598
  else:
598
599
  full_message_data = data
599
- self._processSwitchMessage(message_type, flags, button, payload, full_message_data, oldPos, packet_seq, raw_packet, android_switch_event)
600
+ self._processSwitchMessage(
601
+ message_type,
602
+ flags,
603
+ button,
604
+ payload,
605
+ full_message_data,
606
+ oldPos,
607
+ packet_seq,
608
+ raw_packet,
609
+ )
600
610
  elif message_type == 0x29:
601
611
  # This shouldn't happen due to check above, but just in case
602
- self._logger.debug(f"Ignoring embedded type 0x29 message")
603
- elif message_type in [0x00, 0x06, 0x09, 0x1f, 0x2a]:
612
+ self._logger.debug("Ignoring embedded type 0x29 message")
613
+ elif message_type in [0x00, 0x06, 0x09, 0x1F, 0x2A]:
604
614
  # Known non-switch message types - log at debug level
605
615
  self._logger.debug(
606
616
  f"Non-switch message type 0x{message_type:02x}: flags=0x{flags:02x}, "
@@ -620,83 +630,21 @@ class CasambiClient:
620
630
  f"Ran out of data while parsing switch event packet! "
621
631
  f"Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
622
632
  )
623
-
633
+
634
+ # Log summary of all messages found
635
+ self._logger.info(
636
+ f"[CASAMBI_PARSE_SUMMARY] Packet #{packet_seq}: Found {len(all_messages_found)} messages, "
637
+ f"{switch_events_found} switch events"
638
+ )
639
+ for i, msg in enumerate(all_messages_found):
640
+ self._logger.info(
641
+ f"[CASAMBI_MSG_{i+1}] Type=0x{msg['type']:02x} Pos={msg['pos']} "
642
+ f"Flags=0x{msg['flags']:02x} Param=0x{msg['param']:02x} Payload={msg['payload']}"
643
+ )
644
+
624
645
  if switch_events_found == 0:
625
646
  self._logger.debug(f"No switch events found in packet: {b2a(data)}")
626
647
 
627
- def _parseAndroidProtocol(
628
- self, data: bytes, packet_seq: int = None, raw_packet: bytes = None
629
- ) -> None:
630
- """
631
- Parse using Android app's protocol structure (for comparison only).
632
- This doesn't generate events, just logs what it would detect.
633
-
634
- Android protocol structure:
635
- - 2 bytes: Header/flags
636
- - 1 byte: Command type (29-36 for ButtonEvent0-7)
637
- - 2 bytes: Origin
638
- - 2 bytes: Target (lower byte = type, upper byte = unit ID)
639
- - 2 bytes: Age
640
- - Variable: Payload
641
- """
642
- try:
643
- # Check if data looks like Android protocol format
644
- if len(data) < 9:
645
- return
646
-
647
- # Try to parse as Android protocol
648
- pos = 0
649
- if data[0] in [0x08, 0x10]: # This is BLE protocol, not Android protocol
650
- return
651
-
652
- # Attempt Android protocol parsing
653
- try:
654
- import struct
655
- # Read header
656
- header = struct.unpack_from(">H", data, 0)[0]
657
- command_type = data[2] if len(data) > 2 else 0
658
- origin = struct.unpack_from(">H", data, 3)[0] if len(data) > 4 else 0
659
- target = struct.unpack_from(">H", data, 5)[0] if len(data) > 6 else 0
660
- age = struct.unpack_from(">H", data, 7)[0] if len(data) > 8 else 0
661
-
662
- payload_length = (header & 0x3F) # Lower 6 bits
663
- lifetime = (header >> 11) & 0x0F # Bits 11-14
664
-
665
- target_type = target & 0xFF # Lower byte
666
- unit_id = target >> 8 # Upper byte
667
-
668
- # Check if this is a switch event (target type = 6)
669
- if target_type == 6 and 29 <= command_type <= 36:
670
- button_index = command_type - 29
671
-
672
- if len(data) >= 10:
673
- payload_start = 9
674
- if len(data) >= payload_start + 1:
675
- first_payload_byte = data[payload_start]
676
- param_p = (first_payload_byte >> 3) & 0x0F
677
- param_s = first_payload_byte & 0x07
678
- state = (first_payload_byte & 0x80) >> 7
679
-
680
- self._logger.info(
681
- f"[ANDROID PROTOCOL] Unit {unit_id} Switch event: "
682
- f"button #{button_index} (P{param_p} S{param_s}) = {state} "
683
- f"(1=pressed, 0=released)"
684
- )
685
-
686
- # Compare with what current parsing would detect
687
- self._logger.debug(
688
- f"[ANDROID PROTOCOL] Full parse: header=0x{header:04x}, "
689
- f"cmd={command_type}, origin={origin}, target=0x{target:04x}, "
690
- f"lifetime={lifetime}, age={age}, payload_len={payload_length}"
691
- )
692
- except:
693
- # Not Android protocol format, skip
694
- pass
695
-
696
- except Exception as e:
697
- # Silently ignore - this is just for comparison
698
- pass
699
-
700
648
  def _processSwitchMessage(
701
649
  self,
702
650
  message_type: int,
@@ -717,14 +665,14 @@ class CasambiClient:
717
665
  if message_type == 0x10 and len(payload) >= 3:
718
666
  # Type 0x10: unit_id is at payload[2]
719
667
  unit_id = payload[2]
720
- extra_data = payload[3:] if len(payload) > 3 else b''
668
+ extra_data = payload[3:] if len(payload) > 3 else b""
721
669
  else:
722
670
  # Standard parsing for other message types
723
671
  unit_id = payload[0]
724
- extra_data = b''
672
+ extra_data = b""
725
673
  if len(payload) > 2:
726
674
  extra_data = payload[2:]
727
-
675
+
728
676
  # Extract action based on message type (action SHOULD be different for press vs release)
729
677
  if message_type == 0x10 and len(payload) > 1:
730
678
  # Type 0x10: action is at payload[1]
@@ -736,91 +684,50 @@ class CasambiClient:
736
684
  action = None
737
685
 
738
686
  event_string = "unknown"
739
-
687
+
740
688
  # Different interpretation based on message type
741
689
  if message_type == 0x08:
742
690
  # Type 0x08: Use bit 1 of action for press/release
743
691
  if action is not None:
744
692
  is_release = (action >> 1) & 1
745
693
  event_string = "button_release" if is_release else "button_press"
746
-
747
- # NEW: Enhanced logging for battery-powered switches
748
- self._logger.debug(
749
- f"[TYPE 0x08 STATE] action=0x{action:02x}, release_bit={is_release}, "
750
- f"event={event_string}, unit_id={unit_id}, button={button}"
751
- )
752
694
  elif message_type == 0x10:
753
695
  # Type 0x10: The state byte is at position 9 (0-indexed) from message start
754
- # This applies to all units, including battery-powered switches
696
+ # This applies to all units, not just unit 31
755
697
  # full_data for type 0x10 is the message data starting from position 0
756
698
  state_pos = 9
757
699
  if len(full_data) > state_pos:
758
700
  state_byte = full_data[state_pos]
759
-
760
- # NEW: Enhanced state detection with logging
761
- self._logger.debug(
762
- f"[TYPE 0x10 STATE] State byte at pos {state_pos}: 0x{state_byte:02x}"
763
- )
764
-
765
701
  if state_byte == 0x01:
766
702
  event_string = "button_press"
767
703
  elif state_byte == 0x02:
768
704
  event_string = "button_release"
769
705
  elif state_byte == 0x09:
770
706
  event_string = "button_hold"
771
- elif state_byte == 0x0c:
707
+ elif state_byte == 0x0C:
772
708
  event_string = "button_release_after_hold"
773
709
  else:
774
- # More detailed logging for unknown states
775
- self._logger.info(
776
- f"[TYPE 0x10 STATE] Unknown state byte 0x{state_byte:02x} at pos {state_pos}, "
777
- f"unit_id={unit_id}, button={button}, full_data={b2a(full_data)}"
710
+ self._logger.debug(
711
+ f"Type 0x10: Unknown state byte 0x{state_byte:02x} at message pos {state_pos}"
778
712
  )
779
-
780
- # Enhanced fallback logic for battery-powered switches
781
- # Check multiple patterns that indicate release
782
- if len(extra_data) >= 1:
783
- first_extra = extra_data[0]
784
- if first_extra == 0x12:
785
- event_string = "button_release"
786
- self._logger.debug("[TYPE 0x10 STATE] Detected release via 0x12 pattern")
787
- elif first_extra in [0x00, 0x10]:
788
- event_string = "button_press"
789
- self._logger.debug(f"[TYPE 0x10 STATE] Detected press via 0x{first_extra:02x} pattern")
790
- else:
791
- # Check action byte patterns
792
- if action is not None:
793
- # Common patterns: 0xf8/0xfb = press, 0xfa/0xfc = release
794
- if action in [0xf8, 0xfb, 0xf7, 0xf9]:
795
- event_string = "button_press"
796
- self._logger.debug(f"[TYPE 0x10 STATE] Detected press via action 0x{action:02x}")
797
- elif action in [0xfa, 0xfc, 0xfd, 0xfe]:
798
- event_string = "button_release"
799
- self._logger.debug(f"[TYPE 0x10 STATE] Detected release via action 0x{action:02x}")
800
- else:
801
- event_string = "button_press" # Default to press
802
- self._logger.debug(f"[TYPE 0x10 STATE] Defaulting to press for action 0x{action:02x}")
803
- else:
804
- event_string = "button_press" # Default to press
713
+ # Fallback: check if extra_data starts with 0x12 (indicates release)
714
+ if len(extra_data) >= 1 and extra_data[0] == 0x12:
715
+ event_string = "button_release"
805
716
  else:
806
- event_string = "button_press" # Default to press when no extra data
717
+ event_string = "button_press"
807
718
  else:
808
719
  # Fallback when message is too short
809
- self._logger.warning(
810
- f"[TYPE 0x10 STATE] Message too short for state byte (len={len(full_data)}), "
811
- f"unit_id={unit_id}, button={button}, payload={b2a(payload)}"
812
- )
813
-
814
- # Use enhanced fallback detection
815
720
  if len(extra_data) >= 1 and extra_data[0] == 0x12:
816
721
  event_string = "button_release"
817
- self._logger.debug("[TYPE 0x10 STATE] Using 0x12 pattern for release (short message)")
818
- elif action is not None and action in [0xfa, 0xfc, 0xfd, 0xfe]:
819
- event_string = "button_release"
820
- self._logger.debug(f"[TYPE 0x10 STATE] Using action pattern 0x{action:02x} for release (short message)")
722
+ self._logger.debug(
723
+ "Type 0x10: Using extra_data pattern for release detection"
724
+ )
821
725
  else:
822
- event_string = "button_press" # Default to press
823
- self._logger.debug("[TYPE 0x10 STATE] Defaulting to press (short message)")
726
+ # Cannot determine state
727
+ self._logger.warning(
728
+ f"Type 0x10 message missing state info, unit_id={unit_id}, payload={b2a(payload)}"
729
+ )
730
+ event_string = "unknown"
824
731
 
825
732
  action_display = f"{action:#04x}" if action is not None else "N/A"
826
733
 
@@ -828,31 +735,14 @@ class CasambiClient:
828
735
  f"Switch event (type 0x{message_type:02x}): button={button}, unit_id={unit_id}, "
829
736
  f"action={action_display} ({event_string}), flags=0x{flags:02x}"
830
737
  )
831
-
832
- # Include Android parser comparison if available
833
- android_comparison = None
834
- if android_switch_event:
835
- android_comparison = {
836
- 'unit_id': android_switch_event['unit_id'],
837
- 'button': android_switch_event['button'],
838
- 'state': android_switch_event['state'],
839
- 'param_p': android_switch_event['param_p'],
840
- 'param_s': android_switch_event['param_s'],
841
- 'android_log': android_switch_event['android_log']
842
- }
843
- # Log differences
844
- if android_switch_event['unit_id'] != unit_id:
845
- self._logger.warning(f"Unit ID mismatch: current={unit_id}, android={android_switch_event['unit_id']}")
846
- if android_switch_event['button'] != button:
847
- self._logger.warning(f"Button mismatch: current={button}, android={android_switch_event['button']}")
848
-
849
- # Extract controlling unit if present
850
- controlling_unit = None
851
- # This is redundant since we already return early if unit_id_echo != unit_id
852
- # Removing to avoid confusion
853
-
854
- # Filter out all type 0x08 messages
738
+
739
+ # Log detailed info about type 0x08 messages before filtering
855
740
  if message_type == 0x08:
741
+ self._logger.info(
742
+ f"[CASAMBI_TYPE08_FILTERED] Type 0x08 event detected: button={button}, unit_id={unit_id}, "
743
+ f"action={action_display}, event={event_string}, flags=0x{flags:02x}, "
744
+ f"payload={hexlify(payload).decode()}, extra_data={hexlify(extra_data).decode() if extra_data else 'none'}"
745
+ )
856
746
  self._logger.debug(
857
747
  f"Filtering out type 0x08 event: button={button}, unit_id={unit_id}, "
858
748
  f"action={action_display}, flags=0x{flags:02x}"
@@ -869,13 +759,11 @@ class CasambiClient:
869
759
  "event": event_string,
870
760
  "flags": flags,
871
761
  "extra_data": extra_data,
872
- "controlling_unit": controlling_unit,
873
762
  "packet_sequence": packet_seq,
874
763
  "raw_packet": b2a(raw_packet) if raw_packet else None,
875
764
  "decrypted_data": b2a(full_data),
876
765
  "message_position": start_pos,
877
766
  "payload_hex": b2a(payload),
878
- "android_comparison": android_comparison,
879
767
  },
880
768
  )
881
769
 
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.dev5
3
+ Version: 0.3.7.dev6
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=zjenWQE6EuedFJS87HunDvFLYZnnIO-BmM-zqqXfX1Q,38235
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=lcF6N8QoVBOvtYK8EAND6emAdIVHutLTzA_A3l1wTng,31235
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.dev5.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
16
- casambi_bt_revamped-0.3.7.dev5.dist-info/METADATA,sha256=bVPa_6JqEvAHMjmUZbdjpb2T0bElv2uA69OIQQvZ7Aw,3049
17
- casambi_bt_revamped-0.3.7.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- casambi_bt_revamped-0.3.7.dev5.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
19
- casambi_bt_revamped-0.3.7.dev5.dist-info/RECORD,,
14
+ casambi_bt_revamped-0.3.7.dev6.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
15
+ casambi_bt_revamped-0.3.7.dev6.dist-info/METADATA,sha256=urbfHAz9maj1P6cTNorhXeSrphA6waC3Hu_jD00CUns,3048
16
+ casambi_bt_revamped-0.3.7.dev6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ casambi_bt_revamped-0.3.7.dev6.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
18
+ casambi_bt_revamped-0.3.7.dev6.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