casambi-bt-revamped 0.3.7.dev5__py3-none-any.whl → 0.3.7.dev7__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 +10 -6
- CasambiBt/_client.py +113 -225
- CasambiBt/_unit.py +1 -20
- {casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/METADATA +2 -2
- {casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/RECORD +8 -9
- CasambiBt/_client_android_parser.py +0 -215
- {casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/top_level.txt +0 -0
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
|
|
428
|
+
for switch_handler in self._switchEventCallbacks:
|
|
429
429
|
try:
|
|
430
|
-
|
|
430
|
+
switch_handler(data)
|
|
431
431
|
except Exception:
|
|
432
432
|
self._logger.error(
|
|
433
|
-
f"Exception occurred in switchEventCallback {
|
|
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(
|
|
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(
|
|
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
|
|
417
|
+
|
|
418
|
+
# Store raw encrypted packet for reference
|
|
425
419
|
raw_encrypted_packet = data[:]
|
|
426
420
|
|
|
427
|
-
#
|
|
428
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
507
|
-
|
|
508
|
-
|
|
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(
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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"
|
|
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
|
-
|
|
588
|
-
button = parameter & 0x0F
|
|
588
|
+
button = button_lower
|
|
589
589
|
self._logger.debug(
|
|
590
|
-
f"
|
|
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(
|
|
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(
|
|
603
|
-
elif message_type in [0x00, 0x06, 0x09,
|
|
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,
|
|
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 ==
|
|
707
|
+
elif state_byte == 0x0C:
|
|
772
708
|
event_string = "button_release_after_hold"
|
|
773
709
|
else:
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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"
|
|
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(
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
823
|
-
self._logger.
|
|
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
|
-
#
|
|
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}
|
|
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(
|
{casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.7.
|
|
3
|
+
Version: 0.3.7.dev7
|
|
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
|
|
49
|
+
This library now supports receiving switch button events:
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
52
|
from CasambiBt import Casambi
|
{casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/RECORD
RENAMED
|
@@ -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=
|
|
4
|
-
CasambiBt/_client.py,sha256=
|
|
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=
|
|
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.
|
|
16
|
-
casambi_bt_revamped-0.3.7.
|
|
17
|
-
casambi_bt_revamped-0.3.7.
|
|
18
|
-
casambi_bt_revamped-0.3.7.
|
|
19
|
-
casambi_bt_revamped-0.3.7.
|
|
14
|
+
casambi_bt_revamped-0.3.7.dev7.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
15
|
+
casambi_bt_revamped-0.3.7.dev7.dist-info/METADATA,sha256=euuEUM2dktm9lAZeI8-5FVr913OcTEXUkNppVVHm4F8,3048
|
|
16
|
+
casambi_bt_revamped-0.3.7.dev7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
casambi_bt_revamped-0.3.7.dev7.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
18
|
+
casambi_bt_revamped-0.3.7.dev7.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
|
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev5.dist-info → casambi_bt_revamped-0.3.7.dev7.dist-info}/top_level.txt
RENAMED
|
File without changes
|