casambi-bt-revamped 0.3.7.dev3__py3-none-any.whl → 0.3.7.dev4__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 switch_handler in self._switchEventCallbacks:
428
+ for h in self._switchEventCallbacks:
429
429
  try:
430
- switch_handler(data)
430
+ h(data)
431
431
  except Exception:
432
432
  self._logger.error(
433
- f"Exception occurred in switchEventCallback {switch_handler}.",
433
+ f"Exception occurred in switchEventCallback {h}.",
434
434
  exc_info=True,
435
435
  )
436
436
  else:
@@ -457,9 +457,7 @@ class Casambi:
457
457
  self._unitChangedCallbacks.remove(handler)
458
458
  self._logger.debug(f"Removed unit changed handler {handler}")
459
459
 
460
- def registerSwitchEventHandler(
461
- self, handler: Callable[[dict[str, Any]], None]
462
- ) -> None:
460
+ def registerSwitchEventHandler(self, handler: Callable[[dict[str, Any]], None]) -> None:
463
461
  """Register a new handler for switch events.
464
462
 
465
463
  This handler is called whenever a switch event is received.
@@ -476,9 +474,7 @@ class Casambi:
476
474
  self._switchEventCallbacks.append(handler)
477
475
  self._logger.debug(f"Registered switch event handler {handler}")
478
476
 
479
- def unregisterSwitchEventHandler(
480
- self, handler: Callable[[dict[str, Any]], None]
481
- ) -> None:
477
+ def unregisterSwitchEventHandler(self, handler: Callable[[dict[str, Any]], None]) -> None:
482
478
  """Unregister an existing switch event handler.
483
479
 
484
480
  :param handler: The handler to unregister.
CasambiBt/_client.py CHANGED
@@ -33,6 +33,12 @@ from .errors import ( # noqa: E402
33
33
  UnsupportedProtocolVersion,
34
34
  )
35
35
 
36
+ # Import Android parser for comparison
37
+ try:
38
+ from ._client_android_parser import AndroidPacketParser
39
+ except ImportError:
40
+ AndroidPacketParser = None
41
+
36
42
 
37
43
  @unique
38
44
  class IncommingPacketType(IntEnum):
@@ -414,18 +420,28 @@ class CasambiClient:
414
420
  ) -> None:
415
421
  # TODO: Check incoming counter and direction flag
416
422
  self._inPacketCount += 1
417
-
418
- # Store raw encrypted packet for reference
423
+
424
+ # Store raw encrypted packet for Android parser analysis
419
425
  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.
420
431
 
421
432
  try:
422
- decrypted_data = self._encryptor.decryptAndVerify(
423
- data, data[:4] + self._nonce[4:]
424
- )
433
+ decrypted_data = self._encryptor.decryptAndVerify(data, data[:4] + self._nonce[4:])
425
434
  except InvalidSignature:
426
435
  # We only drop packets with invalid signature here instead of going into an error state
427
436
  self._logger.error(f"Invalid signature for packet {b2a(data)}!")
428
437
  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
429
445
 
430
446
  packetType = decrypted_data[0]
431
447
  self._logger.debug(f"Incoming data of type {packetType}: {b2a(decrypted_data)}")
@@ -433,9 +449,7 @@ class CasambiClient:
433
449
  if packetType == IncommingPacketType.UnitState:
434
450
  self._parseUnitStates(decrypted_data[1:])
435
451
  elif packetType == IncommingPacketType.SwitchEvent:
436
- self._parseSwitchEvent(
437
- decrypted_data[1:], self._inPacketCount, raw_encrypted_packet
438
- )
452
+ self._parseSwitchEvent(decrypted_data[1:], self._inPacketCount, raw_encrypted_packet, android_switch_event)
439
453
  elif packetType == IncommingPacketType.NetworkConfig:
440
454
  # We don't care about the config the network thinks it has.
441
455
  # We assume that cloud config and local config match.
@@ -489,29 +503,23 @@ class CasambiClient:
489
503
  f"Ran out of data while parsing unit state! Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
490
504
  )
491
505
 
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
-
506
+ def _parseSwitchEvent(self, data: bytes, packet_seq: int = None, raw_packet: bytes = None, android_switch_event: dict = None) -> None:
507
+ """Parse switch event packet which contains multiple message types"""
508
+ self._logger.info(f"Parsing incoming switch event packet #{packet_seq}... Data: {b2a(data)}")
509
+
500
510
  # Special handling for message type 0x29 - not a switch event
501
511
  if len(data) >= 1 and data[0] == 0x29:
502
- self._logger.debug(
503
- f"Ignoring message type 0x29 (not a switch event): {b2a(data)}"
504
- )
512
+ self._logger.debug(f"Ignoring message type 0x29 (not a switch event): {b2a(data)}")
505
513
  return
506
514
 
507
515
  pos = 0
508
516
  oldPos = 0
509
517
  switch_events_found = 0
510
-
518
+
511
519
  try:
512
520
  while pos <= len(data) - 3:
513
521
  oldPos = pos
514
-
522
+
515
523
  # Parse message header
516
524
  message_type = data[pos]
517
525
  flags = data[pos + 1]
@@ -546,19 +554,15 @@ class CasambiClient:
546
554
  # Extract button ID - try both upper and lower nibbles
547
555
  button_lower = parameter & 0x0F
548
556
  button_upper = (parameter >> 4) & 0x0F
549
-
557
+
550
558
  # Use upper 4 bits if lower 4 bits are 0, otherwise use lower 4 bits
551
559
  if button_lower == 0 and button_upper != 0:
552
560
  button = button_upper
553
- self._logger.debug(
554
- f"EVO button extraction: parameter=0x{parameter:02x}, using upper nibble, button={button}"
555
- )
561
+ self._logger.debug(f"EVO button extraction: parameter=0x{parameter:02x}, using upper nibble, button={button}")
556
562
  else:
557
563
  button = button_lower
558
- self._logger.debug(
559
- f"EVO button extraction: parameter=0x{parameter:02x}, using lower nibble, button={button}"
560
- )
561
-
564
+ self._logger.debug(f"EVO button extraction: parameter=0x{parameter:02x}, using lower nibble, button={button}")
565
+
562
566
  # For type 0x10 messages, we need to pass additional data beyond the declared payload
563
567
  if message_type == 0x10:
564
568
  # Extend to include at least 10 bytes from message start for state byte
@@ -566,20 +570,11 @@ class CasambiClient:
566
570
  full_message_data = data[oldPos:extended_end]
567
571
  else:
568
572
  full_message_data = data
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
- )
573
+ self._processSwitchMessage(message_type, flags, button, payload, full_message_data, oldPos, packet_seq, raw_packet, android_switch_event)
579
574
  elif message_type == 0x29:
580
575
  # This shouldn't happen due to check above, but just in case
581
- self._logger.debug("Ignoring embedded type 0x29 message")
582
- elif message_type in [0x00, 0x06, 0x09, 0x1F, 0x2A]:
576
+ self._logger.debug(f"Ignoring embedded type 0x29 message")
577
+ elif message_type in [0x00, 0x06, 0x09, 0x1f, 0x2a]:
583
578
  # Known non-switch message types - log at debug level
584
579
  self._logger.debug(
585
580
  f"Non-switch message type 0x{message_type:02x}: flags=0x{flags:02x}, "
@@ -599,22 +594,12 @@ class CasambiClient:
599
594
  f"Ran out of data while parsing switch event packet! "
600
595
  f"Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
601
596
  )
602
-
597
+
603
598
  if switch_events_found == 0:
604
599
  self._logger.debug(f"No switch events found in packet: {b2a(data)}")
605
600
 
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)."""
601
+ def _processSwitchMessage(self, message_type: int, flags: int, button: int, payload: bytes, full_data: bytes, start_pos: int, packet_seq: int = None, raw_packet: bytes = None, android_switch_event: dict = None) -> None:
602
+ """Process a switch/button message (types 0x08 or 0x10)"""
618
603
  if not payload:
619
604
  self._logger.error("Switch message has empty payload")
620
605
  return
@@ -623,14 +608,14 @@ class CasambiClient:
623
608
  if message_type == 0x10 and len(payload) >= 3:
624
609
  # Type 0x10: unit_id is at payload[2]
625
610
  unit_id = payload[2]
626
- extra_data = payload[3:] if len(payload) > 3 else b""
611
+ extra_data = payload[3:] if len(payload) > 3 else b''
627
612
  else:
628
613
  # Standard parsing for other message types
629
614
  unit_id = payload[0]
630
- extra_data = b""
615
+ extra_data = b''
631
616
  if len(payload) > 2:
632
617
  extra_data = payload[2:]
633
-
618
+
634
619
  # Extract action based on message type (action SHOULD be different for press vs release)
635
620
  if message_type == 0x10 and len(payload) > 1:
636
621
  # Type 0x10: action is at payload[1]
@@ -642,7 +627,7 @@ class CasambiClient:
642
627
  action = None
643
628
 
644
629
  event_string = "unknown"
645
-
630
+
646
631
  # Different interpretation based on message type
647
632
  if message_type == 0x08:
648
633
  # Type 0x08: Use bit 1 of action for press/release
@@ -662,12 +647,10 @@ class CasambiClient:
662
647
  event_string = "button_release"
663
648
  elif state_byte == 0x09:
664
649
  event_string = "button_hold"
665
- elif state_byte == 0x0C:
650
+ elif state_byte == 0x0c:
666
651
  event_string = "button_release_after_hold"
667
652
  else:
668
- self._logger.debug(
669
- f"Type 0x10: Unknown state byte 0x{state_byte:02x} at message pos {state_pos}"
670
- )
653
+ self._logger.debug(f"Type 0x10: Unknown state byte 0x{state_byte:02x} at message pos {state_pos}")
671
654
  # Fallback: check if extra_data starts with 0x12 (indicates release)
672
655
  if len(extra_data) >= 1 and extra_data[0] == 0x12:
673
656
  event_string = "button_release"
@@ -677,14 +660,10 @@ class CasambiClient:
677
660
  # Fallback when message is too short
678
661
  if len(extra_data) >= 1 and extra_data[0] == 0x12:
679
662
  event_string = "button_release"
680
- self._logger.debug(
681
- "Type 0x10: Using extra_data pattern for release detection"
682
- )
663
+ self._logger.debug(f"Type 0x10: Using extra_data pattern for release detection")
683
664
  else:
684
665
  # Cannot determine state
685
- self._logger.warning(
686
- f"Type 0x10 message missing state info, unit_id={unit_id}, payload={b2a(payload)}"
687
- )
666
+ self._logger.warning(f"Type 0x10 message missing state info, unit_id={unit_id}, payload={b2a(payload)}")
688
667
  event_string = "unknown"
689
668
 
690
669
  action_display = f"{action:#04x}" if action is not None else "N/A"
@@ -693,11 +672,33 @@ class CasambiClient:
693
672
  f"Switch event (type 0x{message_type:02x}): button={button}, unit_id={unit_id}, "
694
673
  f"action={action_display} ({event_string}), flags=0x{flags:02x}"
695
674
  )
696
-
697
- # Filter out type 0x08 messages with button=0 (likely notifications)
698
- if message_type == 0x08 and button == 0:
675
+
676
+ # Include Android parser comparison if available
677
+ android_comparison = None
678
+ if android_switch_event:
679
+ android_comparison = {
680
+ 'unit_id': android_switch_event['unit_id'],
681
+ 'button': android_switch_event['button'],
682
+ 'state': android_switch_event['state'],
683
+ 'param_p': android_switch_event['param_p'],
684
+ 'param_s': android_switch_event['param_s'],
685
+ 'android_log': android_switch_event['android_log']
686
+ }
687
+ # Log differences
688
+ if android_switch_event['unit_id'] != unit_id:
689
+ self._logger.warning(f"Unit ID mismatch: current={unit_id}, android={android_switch_event['unit_id']}")
690
+ if android_switch_event['button'] != button:
691
+ self._logger.warning(f"Button mismatch: current={button}, android={android_switch_event['button']}")
692
+
693
+ # Extract controlling unit if present
694
+ controlling_unit = None
695
+ # This is redundant since we already return early if unit_id_echo != unit_id
696
+ # Removing to avoid confusion
697
+
698
+ # Filter out all type 0x08 messages
699
+ if message_type == 0x08:
699
700
  self._logger.debug(
700
- f"Filtering out type 0x08 notification event: button={button}, unit_id={unit_id}, "
701
+ f"Filtering out type 0x08 event: button={button}, unit_id={unit_id}, "
701
702
  f"action={action_display}, flags=0x{flags:02x}"
702
703
  )
703
704
  return
@@ -712,11 +713,13 @@ class CasambiClient:
712
713
  "event": event_string,
713
714
  "flags": flags,
714
715
  "extra_data": extra_data,
716
+ "controlling_unit": controlling_unit,
715
717
  "packet_sequence": packet_seq,
716
718
  "raw_packet": b2a(raw_packet) if raw_packet else None,
717
719
  "decrypted_data": b2a(full_data),
718
720
  "message_position": start_pos,
719
721
  "payload_hex": b2a(payload),
722
+ "android_comparison": android_comparison,
720
723
  },
721
724
  )
722
725
 
@@ -0,0 +1,215 @@
1
+ """
2
+ Android App Compatible Parser for Casambi Switch Events
3
+
4
+ This module implements the exact parsing logic from the decompiled Android app
5
+ to compare with our current implementation.
6
+ """
7
+
8
+ import struct
9
+ from typing import Dict, Any, Optional, Tuple
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AndroidPacketParser:
16
+ """Parser that follows the exact Android app implementation"""
17
+
18
+ @staticmethod
19
+ def parse_packet_header(data: bytes, pos: int = 0) -> Tuple[Dict[str, Any], int]:
20
+ """
21
+ Parse packet header according to Android app C1775b.Q method (lines 230-243)
22
+
23
+ Returns: (header_dict, new_position)
24
+ """
25
+ if len(data) - pos < 9:
26
+ raise ValueError("Insufficient data for packet header")
27
+
28
+ # Read header (2 bytes)
29
+ unsigned_short = struct.unpack_from('>H', data, pos)[0]
30
+ pos += 2
31
+
32
+ # Extract flags from header
33
+ has_origin_handle = (unsigned_short & 64) != 0 # bit 6
34
+ is_unique = (unsigned_short & 128) != 0 # bit 7
35
+ has_session = (unsigned_short & 256) != 0 # bit 8
36
+ has_origin_handle_alt = (unsigned_short & 512) != 0 # bit 9
37
+ flag_1024 = (unsigned_short & 1024) != 0 # bit 10
38
+
39
+ # Read command type (1 byte) - this is the EnumC1777d ordinal
40
+ command_type = data[pos] & 0xFF
41
+ pos += 1
42
+
43
+ # Read origin (2 bytes)
44
+ origin = struct.unpack_from('>H', data, pos)[0]
45
+ pos += 2
46
+
47
+ # Read target (2 bytes)
48
+ target = struct.unpack_from('>H', data, pos)[0]
49
+ pos += 2
50
+
51
+ # Extract lifetime from header bits 11-14
52
+ lifetime = (unsigned_short >> 11) & 15
53
+
54
+ # Read age (2 bytes)
55
+ age = struct.unpack_from('>H', data, pos)[0]
56
+ pos += 2
57
+
58
+ # Read optional origin handle if flag is set
59
+ origin_handle = None
60
+ if has_origin_handle_alt:
61
+ origin_handle = data[pos] & 0xFF
62
+ pos += 1
63
+
64
+ # Extract payload length from header bits 0-5
65
+ payload_length = unsigned_short & 63
66
+
67
+ header = {
68
+ 'header_raw': unsigned_short,
69
+ 'command_type': command_type,
70
+ 'origin': origin,
71
+ 'target': target,
72
+ 'lifetime': lifetime,
73
+ 'age': age,
74
+ 'origin_handle': origin_handle,
75
+ 'payload_length': payload_length,
76
+ 'flags': {
77
+ 'has_origin_handle': has_origin_handle,
78
+ 'is_unique': is_unique,
79
+ 'has_session': has_session,
80
+ 'has_origin_handle_alt': has_origin_handle_alt,
81
+ 'flag_1024': flag_1024
82
+ }
83
+ }
84
+
85
+ return header, pos
86
+
87
+ @staticmethod
88
+ def parse_switch_event(header: Dict[str, Any], payload: bytes) -> Optional[Dict[str, Any]]:
89
+ """
90
+ Parse switch event according to Android app logic (lines 271-280)
91
+
92
+ The Android app checks:
93
+ - target & 0xFF == 6 (lower byte of target must be 6)
94
+ - command_type ordinal must be between 29-36 (FunctionButtonEvent0-7)
95
+ - payload must have at least 3 bytes
96
+ """
97
+ target_type = header['target'] & 0xFF
98
+ target_unit_id = header['target'] >> 8
99
+ command_type = header['command_type']
100
+
101
+ # Check if this is a switch event
102
+ if target_type != 6:
103
+ return None
104
+
105
+ # Check if command type is in range for button events (29-36)
106
+ if command_type < 29 or command_type > 36:
107
+ return None
108
+
109
+ # Check payload length
110
+ if len(payload) < 3:
111
+ logger.warning(f"Switch event payload too short: {len(payload)} bytes")
112
+ return None
113
+
114
+ # Parse according to Android logic
115
+ first_byte = payload[0]
116
+ button_index = command_type - 29 # 0-7
117
+
118
+ # Extract parameters from first byte
119
+ param_p = (first_byte >> 3) & 15 # bits 3-6
120
+ param_s = first_byte & 7 # bits 0-2
121
+ state = 1 if (first_byte & 128) else 0 # bit 7
122
+
123
+ return {
124
+ 'unit_id': target_unit_id,
125
+ 'button': button_index,
126
+ 'state': state, # 1 = pressed, 0 = released
127
+ 'param_p': param_p,
128
+ 'param_s': param_s,
129
+ 'target_type': target_type,
130
+ 'command_type': command_type,
131
+ 'payload_hex': payload.hex(),
132
+ 'android_log': f"Unit {target_unit_id} Switch event: #{button_index} (P{param_p} S{param_s}) = {state}"
133
+ }
134
+
135
+ @staticmethod
136
+ def parse_complete_packet(data: bytes) -> Dict[str, Any]:
137
+ """Parse a complete packet and extract switch events if present"""
138
+ try:
139
+ header, payload_start = AndroidPacketParser.parse_packet_header(data)
140
+
141
+ # Read payload
142
+ payload_length = header['payload_length']
143
+ if len(data) - payload_start < payload_length:
144
+ raise ValueError(f"Insufficient data for payload: need {payload_length}, have {len(data) - payload_start}")
145
+
146
+ payload = data[payload_start:payload_start + payload_length]
147
+
148
+ # Try to parse as switch event
149
+ switch_event = AndroidPacketParser.parse_switch_event(header, payload)
150
+
151
+ return {
152
+ 'header': header,
153
+ 'payload': payload.hex(),
154
+ 'switch_event': switch_event
155
+ }
156
+
157
+ except Exception as e:
158
+ logger.error(f"Failed to parse packet: {e}")
159
+ return {'error': str(e), 'data': data.hex()}
160
+
161
+
162
+ # Command type constants from EnumC1777d
163
+ class FunctionType:
164
+ """Function type constants from Android app"""
165
+ BUTTON_EVENT_0 = 29
166
+ BUTTON_EVENT_1 = 30
167
+ BUTTON_EVENT_2 = 31
168
+ BUTTON_EVENT_3 = 32
169
+ BUTTON_EVENT_4 = 33
170
+ BUTTON_EVENT_5 = 34
171
+ BUTTON_EVENT_6 = 35
172
+ BUTTON_EVENT_7 = 36
173
+
174
+
175
+ def compare_with_current_parser(data: bytes, current_parser_result: Dict[str, Any]) -> Dict[str, Any]:
176
+ """Compare Android parser results with current implementation"""
177
+ android_result = AndroidPacketParser.parse_complete_packet(data)
178
+
179
+ comparison = {
180
+ 'android_parser': android_result,
181
+ 'current_parser': current_parser_result,
182
+ 'differences': []
183
+ }
184
+
185
+ # Compare results
186
+ if android_result.get('switch_event') and current_parser_result:
187
+ android_evt = android_result['switch_event']
188
+
189
+ # Check unit_id
190
+ if android_evt['unit_id'] != current_parser_result.get('unit_id'):
191
+ comparison['differences'].append({
192
+ 'field': 'unit_id',
193
+ 'android': android_evt['unit_id'],
194
+ 'current': current_parser_result.get('unit_id')
195
+ })
196
+
197
+ # Check button
198
+ if android_evt['button'] != current_parser_result.get('button'):
199
+ comparison['differences'].append({
200
+ 'field': 'button',
201
+ 'android': android_evt['button'],
202
+ 'current': current_parser_result.get('button')
203
+ })
204
+
205
+ # Map Android state to current event names
206
+ android_event = 'button_press' if android_evt['state'] == 1 else 'button_release'
207
+ if android_event != current_parser_result.get('event'):
208
+ comparison['differences'].append({
209
+ 'field': 'event',
210
+ 'android': android_event,
211
+ 'current': current_parser_result.get('event'),
212
+ 'note': 'Android only has press/release, no hold detection'
213
+ })
214
+
215
+ return comparison
CasambiBt/_network.py CHANGED
@@ -382,6 +382,7 @@ class Network:
382
382
  unitTypeJson["mode"],
383
383
  unitTypeJson["stateLength"],
384
384
  controls,
385
+ unitTypeJson.get("pushButtonCount"),
385
386
  )
386
387
 
387
388
  # Chache unit type
CasambiBt/_unit.py CHANGED
@@ -75,6 +75,7 @@ class UnitType:
75
75
  :ivar model: The model name of this unit type.
76
76
  :ivar manufacturer: The manufacturer of this unit type.
77
77
  :ivar controls: The different types of controls this unit type is capable of.
78
+ :ivar pushButtonCount: The number of push buttons this unit type has (optional).
78
79
  """
79
80
 
80
81
  id: int
@@ -83,6 +84,7 @@ class UnitType:
83
84
  mode: str
84
85
  stateLength: int
85
86
  controls: list[UnitControl]
87
+ pushButtonCount: int | None = None
86
88
 
87
89
  def get_control(self, controlType: UnitControlType) -> UnitControl | None:
88
90
  """Return the control description if the unit type supports the given type of control.
@@ -111,6 +113,7 @@ class UnitState:
111
113
  self._colorsource: ColorSource | None = None
112
114
  self._xy: tuple[float, float] | None = None
113
115
  self._slider: int | None = None
116
+ self._onoff: bool | None = None
114
117
 
115
118
  def _check_range(
116
119
  self, value: int | float, min: int | float, max: int | float
@@ -269,8 +272,20 @@ class UnitState:
269
272
  def slider(self) -> None:
270
273
  self.slider = None
271
274
 
275
+ @property
276
+ def onoff(self) -> bool | None:
277
+ return self._onoff
278
+
279
+ @onoff.setter
280
+ def onoff(self, value: bool) -> None:
281
+ self._onoff = value
282
+
283
+ @onoff.deleter
284
+ def onoff(self) -> None:
285
+ self._onoff = None
286
+
272
287
  def __repr__(self) -> str:
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})"
288
+ 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})"
274
289
 
275
290
 
276
291
  # TODO: Make unit immutable (refactor state, on, online out of it)
@@ -308,6 +323,8 @@ class Unit:
308
323
  @property
309
324
  def is_on(self) -> bool:
310
325
  """Determine whether the unit is turned on."""
326
+ if self.unitType.get_control(UnitControlType.ONOFF) and self._state:
327
+ return self._on and self._state.onoff is True
311
328
  if self.unitType.get_control(UnitControlType.DIMMER) and self._state:
312
329
  return (
313
330
  self._on and self._state.dimmer is not None and self._state.dimmer > 0
@@ -385,6 +402,8 @@ class Unit:
385
402
  elif c.type == UnitControlType.SLIDER and state.slider is not None:
386
403
  scale = UnitState.SLIDER_RESOLUTION - c.length
387
404
  scaledValue = state.slider >> scale
405
+ elif c.type == UnitControlType.ONOFF and state.onoff is not None:
406
+ scaledValue = 1 if state.onoff else 0
388
407
 
389
408
  # Use default if unsupported type or unset value in state
390
409
  else:
@@ -477,6 +496,8 @@ class Unit:
477
496
  elif c.type == UnitControlType.SLIDER:
478
497
  scale = UnitState.SLIDER_RESOLUTION - c.length
479
498
  self._state.slider = cInt << scale
499
+ elif c.type == UnitControlType.ONOFF:
500
+ self._state.onoff = cInt != 0
480
501
  elif c.type == UnitControlType.UNKOWN:
481
502
  # Might be useful for implementing more state types
482
503
  _LOGGER.debug(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.7.dev3
3
+ Version: 0.3.7.dev4
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 library now supports receiving switch button events:
49
+ This fork adds support for receiving switch button events:
50
50
 
51
51
  ```python
52
52
  from CasambiBt import Casambi
@@ -1,18 +1,19 @@
1
1
  CasambiBt/__init__.py,sha256=TW445xSu5PV3TyMjJfwaA1JoWvQQ8LXhZgGdDTfWf3s,302
2
2
  CasambiBt/_cache.py,sha256=KZ2xbiHAHXUPa8Gw_75Nw9NL4QSY_sTWHbyYXYUDaB0,3865
3
- CasambiBt/_casambi.py,sha256=tQgmG-8lHbl4_FDS7NwPrucrqcQZd2kimcJa43TYFaw,23156
4
- CasambiBt/_client.py,sha256=2kHgkf3ERobTDF47ZMNSFl5WrEkWYgDSXfh6R6PsU0Q,28926
3
+ CasambiBt/_casambi.py,sha256=gLLkhEcObgapqTx5Mk7WRClyG29UyfZYZCCIhhOg4H4,23101
4
+ CasambiBt/_client.py,sha256=8a1sYxvuNStb7ldgeEHDFyonPIfPTIjFtqZQIbDo62U,30511
5
+ CasambiBt/_client_android_parser.py,sha256=1lVkVYMO0Nhh9_nkNwgb68hlCS_uD8WxYQDir5hOdHs,7744
5
6
  CasambiBt/_constants.py,sha256=_AxkG7Btxl4VeS6mO7GJW5Kc9dFs3s9sDmtJ83ZEKNw,359
6
7
  CasambiBt/_discover.py,sha256=H7HpiFYIy9ELvmPXXd_ck-5O5invJf15dDIRk-vO5IE,1696
7
8
  CasambiBt/_encryption.py,sha256=CLcoOOrggQqhJbnr_emBnEnkizpWDvb_0yFnitq4_FM,3831
8
9
  CasambiBt/_keystore.py,sha256=Jdiq0zMPDmhfpheSojKY6sTUpmVrvX_qOyO7yCYd3kw,2788
9
- CasambiBt/_network.py,sha256=qcsWn_EsBexzXCv14JcpSIymhuR6Eaf479lZdzpfYBM,14417
10
+ CasambiBt/_network.py,sha256=B5uVDrfwFKZ3UQgf5THUwasBxh1nzl7KJlq9CqoIyqU,14466
10
11
  CasambiBt/_operation.py,sha256=-BuC1Bvtg-G-zSN_b_0JMvXdHZaR6LbTw0S425jg96c,842
11
- CasambiBt/_unit.py,sha256=M-Q8-Xd3qjJSUEvsFtic8E4xDc_gtWYakbTGyoIA-P8,16377
12
+ CasambiBt/_unit.py,sha256=B1ce5MaNdoF9ljHAhoiRfV1Tn6CBM4oD3OPDmShIMm8,17167
12
13
  CasambiBt/errors.py,sha256=0JgDjaKlAKDes0poWzA8nrTUYQ8qdNfBb8dfaqqzCRA,1664
13
14
  CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- casambi_bt_revamped-0.3.7.dev3.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
15
- casambi_bt_revamped-0.3.7.dev3.dist-info/METADATA,sha256=grmzh3IRXluM845lDfwqMJHeP-2QlqslRyOI-wlebAk,3048
16
- casambi_bt_revamped-0.3.7.dev3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- casambi_bt_revamped-0.3.7.dev3.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
18
- casambi_bt_revamped-0.3.7.dev3.dist-info/RECORD,,
15
+ casambi_bt_revamped-0.3.7.dev4.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
16
+ casambi_bt_revamped-0.3.7.dev4.dist-info/METADATA,sha256=7DgCkhLn7OMDycRzDzB7EevVTt1gZqnGr5UVCvbemZo,3049
17
+ casambi_bt_revamped-0.3.7.dev4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ casambi_bt_revamped-0.3.7.dev4.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
19
+ casambi_bt_revamped-0.3.7.dev4.dist-info/RECORD,,