casambi-bt-revamped 0.3.7.dev2__tar.gz → 0.3.7.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {casambi_bt_revamped-0.3.7.dev2/src/casambi_bt_revamped.egg-info → casambi_bt_revamped-0.3.7.dev3}/PKG-INFO +2 -2
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/README.md +1 -1
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/setup.cfg +1 -1
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_casambi.py +10 -6
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_client.py +72 -75
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_unit.py +1 -20
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3/src/casambi_bt_revamped.egg-info}/PKG-INFO +2 -2
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/casambi_bt_revamped.egg-info/SOURCES.txt +0 -1
- casambi_bt_revamped-0.3.7.dev2/src/CasambiBt/_client_android_parser.py +0 -215
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/LICENSE +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/pyproject.toml +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/__init__.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_cache.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_constants.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_discover.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_encryption.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_keystore.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_network.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_operation.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/errors.py +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/py.typed +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/casambi_bt_revamped.egg-info/dependency_links.txt +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
- {casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/casambi_bt_revamped.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.7.
|
|
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
|
|
49
|
+
This library now supports receiving switch button events:
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
52
|
from CasambiBt import Casambi
|
|
@@ -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.
|
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
507
|
-
|
|
508
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
577
|
-
elif message_type in [0x00, 0x06, 0x09,
|
|
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(
|
|
602
|
-
|
|
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 ==
|
|
665
|
+
elif state_byte == 0x0C:
|
|
651
666
|
event_string = "button_release_after_hold"
|
|
652
667
|
else:
|
|
653
|
-
self._logger.debug(
|
|
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(
|
|
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(
|
|
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,33 +693,11 @@ 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
|
-
#
|
|
677
|
-
|
|
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
|
-
|
|
698
|
-
# Filter out ALL type 0x08 messages (broadcast notifications with incorrect button numbers)
|
|
699
|
-
if message_type == 0x08:
|
|
696
|
+
|
|
697
|
+
# Filter out type 0x08 messages with button=0 (likely notifications)
|
|
698
|
+
if message_type == 0x08 and button == 0:
|
|
700
699
|
self._logger.debug(
|
|
701
|
-
f"Filtering out type 0x08
|
|
700
|
+
f"Filtering out type 0x08 notification event: button={button}, unit_id={unit_id}, "
|
|
702
701
|
f"action={action_display}, flags=0x{flags:02x}"
|
|
703
702
|
)
|
|
704
703
|
return
|
|
@@ -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
|
|
|
@@ -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(
|
|
@@ -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.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
|
|
49
|
+
This library now supports receiving switch button events:
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
52
|
from CasambiBt import Casambi
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_constants.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_discover.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_encryption.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_keystore.py
RENAMED
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev2 → casambi_bt_revamped-0.3.7.dev3}/src/CasambiBt/_operation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|