casambi-bt-revamped 0.3.11__tar.gz → 0.3.12.dev1__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.
Files changed (27) hide show
  1. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/PKG-INFO +45 -6
  2. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/README.md +44 -5
  3. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/setup.cfg +1 -1
  4. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_casambi.py +11 -7
  5. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_client.py +144 -147
  6. casambi_bt_revamped-0.3.12.dev1/src/CasambiBt/_invocation.py +116 -0
  7. casambi_bt_revamped-0.3.12.dev1/src/CasambiBt/_switch_events.py +327 -0
  8. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_unit.py +37 -1
  9. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/casambi_bt_revamped.egg-info/PKG-INFO +45 -6
  10. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/casambi_bt_revamped.egg-info/SOURCES.txt +5 -1
  11. casambi_bt_revamped-0.3.12.dev1/tests/test_switch_event_logs.py +205 -0
  12. casambi_bt_revamped-0.3.12.dev1/tests/test_unit_state_logs.py +124 -0
  13. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/LICENSE +0 -0
  14. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/pyproject.toml +0 -0
  15. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/__init__.py +0 -0
  16. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_cache.py +0 -0
  17. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_constants.py +0 -0
  18. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_discover.py +0 -0
  19. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_encryption.py +0 -0
  20. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_keystore.py +0 -0
  21. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_network.py +0 -0
  22. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/_operation.py +0 -0
  23. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/errors.py +0 -0
  24. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/CasambiBt/py.typed +0 -0
  25. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/casambi_bt_revamped.egg-info/dependency_links.txt +0 -0
  26. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
  27. {casambi_bt_revamped-0.3.11 → casambi_bt_revamped-0.3.12.dev1}/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.11
3
+ Version: 0.3.12.dev1
4
4
  Summary: Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
5
5
  Home-page: https://github.com/rankjie/casambi-bt
6
6
  Author: rankjie
@@ -26,7 +26,7 @@ Dynamic: license-file
26
26
 
27
27
  This is a customized fork of the original [casambi-bt](https://github.com/lkempf/casambi-bt) library with additional features and should only be used for special needs:
28
28
 
29
- - **Switch event support** - Receive button press/release events from Casambi switches
29
+ - **Switch event support** - Receive button press/release/hold events from Casambi switches (wired + wireless)
30
30
  - **Improved relay status handling** - Better support for relay units
31
31
  - **Bug fixes and improvements** - Various fixes based on real-world usage
32
32
 
@@ -46,15 +46,38 @@ 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 library supports receiving physical switch events as a decoded stream of INVOCATION frames (ground truth from the official Android app).
50
+
51
+ Event types you can expect:
52
+ - `button_press`
53
+ - `button_release`
54
+ - `button_hold`
55
+ - `button_release_after_hold`
56
+ - `input_event` (raw NotifyInput frame that may accompany presses/holds; useful for diagnostics and some wired devices)
50
57
 
51
58
  ```python
52
59
  from CasambiBt import Casambi
53
60
 
54
61
  def handle_switch_event(event_data):
55
- print(f"Switch event: Unit {event_data['unit_id']}, "
56
- f"Button {event_data['button']}, "
57
- f"Action: {event_data['event']}")
62
+ print(
63
+ "Switch event:",
64
+ {
65
+ "unit_id": event_data.get("unit_id"),
66
+ "button": event_data.get("button"),
67
+ "event": event_data.get("event"),
68
+ # INVOCATION metadata (useful for debugging/correlation)
69
+ "event_id": event_data.get("event_id"),
70
+ "opcode": event_data.get("opcode"),
71
+ "target_type": event_data.get("target_type"),
72
+ "origin": event_data.get("origin"),
73
+ "age": event_data.get("age"),
74
+ # NotifyInput fields (target_type=0x12)
75
+ "input_code": event_data.get("input_code"),
76
+ "input_channel": event_data.get("input_channel"),
77
+ "input_value16": event_data.get("input_value16"),
78
+ "input_mapped_event": event_data.get("input_mapped_event"),
79
+ },
80
+ )
58
81
 
59
82
  casa = Casambi()
60
83
  # ... connect to network ...
@@ -65,6 +88,13 @@ casa.registerSwitchEventHandler(handle_switch_event)
65
88
  # Events will be received when buttons are pressed/released
66
89
  ```
67
90
 
91
+ Notes:
92
+ - Wireless (battery) switches typically send a "button stream" (target_type `0x06`) for press/release, and a NotifyInput stream (target_type `0x12`) for hold/release-after-hold.
93
+ - Wired switches often only send NotifyInput (target_type `0x12`), so `input_code` is mapped into `button_press/button_release/...` when appropriate.
94
+ - The library suppresses same-state retransmits at the protocol layer (edge detection), so Home Assistant-style time-window deduplication should generally not be necessary.
95
+
96
+ For the parsing details and field layout, see `doc/PROTOCOL_PARSING.md`.
97
+
68
98
  ### MacOS
69
99
 
70
100
  MacOS [does not expose the Bluetooth MAC address via their official API](https://github.com/hbldh/bleak/issues/140),
@@ -79,3 +109,12 @@ If you have problems connecting to the network please check that your network is
79
109
  ![Gateway settings](/doc/img/gateway.png)
80
110
  ![Network settings](/doc/img/network.png)
81
111
  ![Performance settings](/doc/img/perf.png)
112
+
113
+ ## Development / Offline Testing
114
+
115
+ This repo includes log-driven unit tests for switch parsing:
116
+
117
+ ```bash
118
+ cd casambi-bt
119
+ python -m unittest -v
120
+ ```
@@ -5,7 +5,7 @@
5
5
 
6
6
  This is a customized fork of the original [casambi-bt](https://github.com/lkempf/casambi-bt) library with additional features and should only be used for special needs:
7
7
 
8
- - **Switch event support** - Receive button press/release events from Casambi switches
8
+ - **Switch event support** - Receive button press/release/hold events from Casambi switches (wired + wireless)
9
9
  - **Improved relay status handling** - Better support for relay units
10
10
  - **Bug fixes and improvements** - Various fixes based on real-world usage
11
11
 
@@ -25,15 +25,38 @@ Have a look at `demo.py` for a small example.
25
25
 
26
26
  ### Switch Event Support
27
27
 
28
- This library now supports receiving switch button events:
28
+ This library supports receiving physical switch events as a decoded stream of INVOCATION frames (ground truth from the official Android app).
29
+
30
+ Event types you can expect:
31
+ - `button_press`
32
+ - `button_release`
33
+ - `button_hold`
34
+ - `button_release_after_hold`
35
+ - `input_event` (raw NotifyInput frame that may accompany presses/holds; useful for diagnostics and some wired devices)
29
36
 
30
37
  ```python
31
38
  from CasambiBt import Casambi
32
39
 
33
40
  def handle_switch_event(event_data):
34
- print(f"Switch event: Unit {event_data['unit_id']}, "
35
- f"Button {event_data['button']}, "
36
- f"Action: {event_data['event']}")
41
+ print(
42
+ "Switch event:",
43
+ {
44
+ "unit_id": event_data.get("unit_id"),
45
+ "button": event_data.get("button"),
46
+ "event": event_data.get("event"),
47
+ # INVOCATION metadata (useful for debugging/correlation)
48
+ "event_id": event_data.get("event_id"),
49
+ "opcode": event_data.get("opcode"),
50
+ "target_type": event_data.get("target_type"),
51
+ "origin": event_data.get("origin"),
52
+ "age": event_data.get("age"),
53
+ # NotifyInput fields (target_type=0x12)
54
+ "input_code": event_data.get("input_code"),
55
+ "input_channel": event_data.get("input_channel"),
56
+ "input_value16": event_data.get("input_value16"),
57
+ "input_mapped_event": event_data.get("input_mapped_event"),
58
+ },
59
+ )
37
60
 
38
61
  casa = Casambi()
39
62
  # ... connect to network ...
@@ -44,6 +67,13 @@ casa.registerSwitchEventHandler(handle_switch_event)
44
67
  # Events will be received when buttons are pressed/released
45
68
  ```
46
69
 
70
+ Notes:
71
+ - Wireless (battery) switches typically send a "button stream" (target_type `0x06`) for press/release, and a NotifyInput stream (target_type `0x12`) for hold/release-after-hold.
72
+ - Wired switches often only send NotifyInput (target_type `0x12`), so `input_code` is mapped into `button_press/button_release/...` when appropriate.
73
+ - The library suppresses same-state retransmits at the protocol layer (edge detection), so Home Assistant-style time-window deduplication should generally not be necessary.
74
+
75
+ For the parsing details and field layout, see `doc/PROTOCOL_PARSING.md`.
76
+
47
77
  ### MacOS
48
78
 
49
79
  MacOS [does not expose the Bluetooth MAC address via their official API](https://github.com/hbldh/bleak/issues/140),
@@ -58,3 +88,12 @@ If you have problems connecting to the network please check that your network is
58
88
  ![Gateway settings](/doc/img/gateway.png)
59
89
  ![Network settings](/doc/img/network.png)
60
90
  ![Performance settings](/doc/img/perf.png)
91
+
92
+ ## Development / Offline Testing
93
+
94
+ This repo includes log-driven unit tests for switch parsing:
95
+
96
+ ```bash
97
+ cd casambi-bt
98
+ python -m unittest -v
99
+ ```
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = casambi-bt-revamped
3
- version = 0.3.11
3
+ version = 0.3.12.dev1
4
4
  author = rankjie
5
5
  author_email = rankjie@gmail.com
6
6
  description = Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
@@ -473,13 +473,17 @@ class Casambi:
473
473
  """Register a new handler for switch events.
474
474
 
475
475
  This handler is called whenever a switch event is received.
476
- The handler is supplied with a dictionary containing:
477
- - unit_id: The ID of the switch unit
478
- - button: The button number that was pressed/released
479
- - event: Either "button_press" or "button_release"
480
- - message_type: The raw message type (0x08 or 0x10)
481
- - flags: Additional flags from the message
482
- - extra_data: Any additional data from the message
476
+ The handler is supplied with a dictionary containing (at minimum):
477
+ - unit_id: target unit id (from INVOCATION target high byte)
478
+ - button: best-effort "label" (typically 1..4 for 4-gang switches)
479
+ - event: "button_press" | "button_release" | "input_event"
480
+
481
+ Switch events are parsed from decrypted packet type=7 (INVOCATION stream),
482
+ matching casambi-android `v1.C1775b.Q(Q2.h)`. Extra diagnostic keys include:
483
+ - invocation_flags, opcode, origin, target, target_type, age, origin_handle
484
+ - button_event_index (0..7), param_p, param_s
485
+ - input_index (0..7), input_code, input_b1, input_channel, input_value16, input_mapped_event
486
+ - packet_sequence, arrival_sequence, raw_packet, decrypted_data, payload_hex, frame_offset, event_id
483
487
 
484
488
  :param handler: The method to call when a switch event is received.
485
489
  """
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import inspect
3
3
  import logging
4
+ import platform
4
5
  import struct
5
6
  from binascii import b2a_hex as b2a
6
7
  from collections.abc import Callable
@@ -24,6 +25,7 @@ from cryptography.hazmat.primitives.asymmetric import ec
24
25
  from ._constants import CASA_AUTH_CHAR_UUID, ConnectionState
25
26
  from ._encryption import Encryptor
26
27
  from ._network import Network
28
+ from ._switch_events import SwitchEventStreamDecoder
27
29
 
28
30
  # We need to move these imports here to prevent a cycle.
29
31
  from .errors import ( # noqa: E402
@@ -43,7 +45,7 @@ class IncommingPacketType(IntEnum):
43
45
 
44
46
 
45
47
  MIN_VERSION: Final[int] = 10
46
- MAX_VERSION: Final[int] = 10
48
+ MAX_VERSION: Final[int] = 11
47
49
 
48
50
 
49
51
  class CasambiClient:
@@ -79,6 +81,7 @@ class CasambiClient:
79
81
  else address_or_device
80
82
  )
81
83
  self._logger = logging.getLogger(__name__)
84
+ self._switchDecoder = SwitchEventStreamDecoder(self._logger)
82
85
  self._connectionState: ConnectionState = ConnectionState.NONE
83
86
  self._dataCallback = dataCallback
84
87
  self._disconnectedCallback = disonnectedCallback
@@ -122,6 +125,33 @@ class CasambiClient:
122
125
  else await get_device(self.address)
123
126
  )
124
127
 
128
+ if not device and isinstance(self._address_or_devive, str) and platform.system() == "Darwin":
129
+ # macOS CoreBluetooth typically reports random per-device identifiers as addresses
130
+ # unless `use_bdaddr` is enabled. Our `discover()` uses that flag so try it here.
131
+ try:
132
+ from ._discover import discover as discover_networks # local import to avoid cycles
133
+
134
+ networks = await discover_networks()
135
+ wanted = self.address.replace(":", "").lower()
136
+ for d in networks:
137
+ if d.address.replace(":", "").lower() == wanted:
138
+ device = d
139
+ break
140
+
141
+ if not device:
142
+ self._logger.warning(
143
+ "macOS BLE lookup by address failed. Discovered %d Casambi networks, but none match %s. Discovered=%s",
144
+ len(networks),
145
+ self.address,
146
+ [d.address for d in networks[:10]],
147
+ )
148
+ except Exception:
149
+ self._logger.debug(
150
+ "macOS fallback discovery failed while trying to find %s.",
151
+ self.address,
152
+ exc_info=True,
153
+ )
154
+
125
155
  if not device:
126
156
  self._logger.error("Failed to discover client.")
127
157
  raise NetworkNotFoundError
@@ -453,6 +483,19 @@ class CasambiClient:
453
483
  if packetType == IncommingPacketType.UnitState:
454
484
  self._parseUnitStates(decrypted_data[1:])
455
485
  elif packetType == IncommingPacketType.SwitchEvent:
486
+ # Stable logs for offline analysis: packet seq + encrypted + decrypted.
487
+ # (Decrypted data includes the leading packet type byte.)
488
+ self._logger.info(
489
+ "[CASAMBI_RAW_PACKET] Encrypted #%s: %s",
490
+ device_sequence,
491
+ b2a(raw_encrypted_packet),
492
+ )
493
+ self._logger.info(
494
+ "[CASAMBI_DECRYPTED] Type=%d #%s: %s",
495
+ packetType,
496
+ device_sequence,
497
+ b2a(decrypted_data),
498
+ )
456
499
  # Pass the device sequence as the packet sequence for consumers,
457
500
  # and still include the raw encrypted packet for diagnostics.
458
501
  seq_for_consumer = device_sequence if device_sequence is not None else self._inPacketCount
@@ -469,184 +512,138 @@ class CasambiClient:
469
512
  self._logger.info(f"Packet type {packetType} not implemented. Ignoring!")
470
513
 
471
514
  def _parseUnitStates(self, data: bytes) -> None:
515
+ # Ground truth: casambi-android `v1.C1775b.V(Q2.h)` parses decrypted packet type=6
516
+ # as a stream of unit state records. Records have optional bytes depending on flags.
472
517
  self._logger.info("Parsing incoming unit states...")
473
- self._logger.debug(f"Incoming unit state: {b2a(data)}")
518
+ self._logger.debug("Incoming unit state: %s", b2a(data))
474
519
 
475
520
  pos = 0
476
521
  oldPos = 0
477
522
  try:
523
+ # Android uses `while (available() >= 4)` as the loop condition.
478
524
  while pos <= len(data) - 4:
479
- id = data[pos]
525
+ unit_id = data[pos]
480
526
  flags = data[pos + 1]
481
- stateLen = ((data[pos + 2] >> 4) & 15) + 1
482
- prio = data[pos + 2] & 15
527
+ b8 = data[pos + 2]
528
+ state_len = ((b8 >> 4) & 0x0F) + 1
529
+ prio = b8 & 0x0F
483
530
  pos += 3
484
531
 
485
- online = flags & 2 != 0
486
- on = flags & 1 != 0
487
-
488
- if flags & 4:
489
- pos += 1 # TODO: con?
490
- if flags & 8:
491
- pos += 1 # TODO: sid?
492
- if flags & 16:
493
- pos += 1 # Unkown value
532
+ online = (flags & 0x02) != 0
533
+ on = (flags & 0x01) != 0
534
+
535
+ con: int | None = None
536
+ sid: int | None = None
537
+
538
+ # Optional bytes, matching Android:
539
+ # - flags&0x04: con (1 byte)
540
+ # - flags&0x08: sid (1 byte)
541
+ # - flags&0x10: extra byte; if missing Android uses 0xFF
542
+ if flags & 0x04:
543
+ con = data[pos]
544
+ pos += 1
545
+ if flags & 0x08:
546
+ sid = data[pos]
547
+ pos += 1
548
+
549
+ if flags & 0x10:
550
+ extra_byte = data[pos]
551
+ pos += 1
552
+ else:
553
+ extra_byte = 0xFF
494
554
 
495
- state = data[pos : pos + stateLen]
496
- pos += stateLen
555
+ state = data[pos : pos + state_len]
556
+ pos += state_len
497
557
 
498
- pos += (flags >> 6) & 3 # Padding?
558
+ padding_len = (flags >> 6) & 0x03
559
+ padding = data[pos : pos + padding_len] if padding_len else b""
560
+ pos += padding_len
499
561
 
500
562
  self._logger.debug(
501
- f"Parsed state: Id {id}, prio {prio}, online {online}, on {on}, state {b2a(state)}1"
563
+ "[CASAMBI_UNITSTATE_PARSED] unit=%d flags=0x%02x prio=%d online=%s on=%s con=%s sid=%s extra_byte=%d state=%s padding=%s",
564
+ unit_id,
565
+ flags,
566
+ prio,
567
+ online,
568
+ on,
569
+ con,
570
+ sid,
571
+ extra_byte,
572
+ b2a(state),
573
+ b2a(padding),
502
574
  )
503
575
 
504
576
  self._dataCallback(
505
577
  IncommingPacketType.UnitState,
506
- {"id": id, "online": online, "on": on, "state": state},
578
+ {
579
+ "id": unit_id,
580
+ "online": online,
581
+ "on": on,
582
+ "state": state,
583
+ # Additional fields for diagnostics/analysis
584
+ "flags": flags,
585
+ "prio": prio,
586
+ "state_len": state_len,
587
+ "padding_len": padding_len,
588
+ "con": con,
589
+ "sid": sid,
590
+ "extra_byte": extra_byte,
591
+ "extra_float": extra_byte / 255.0,
592
+ },
507
593
  )
508
594
 
509
595
  oldPos = pos
510
596
  except IndexError:
511
597
  self._logger.error(
512
- f"Ran out of data while parsing unit state! Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
598
+ "Ran out of data while parsing unit state! Remaining data %s in %s.",
599
+ b2a(data[oldPos:]),
600
+ b2a(data),
513
601
  )
514
602
 
515
603
  def _parseSwitchEvent(
516
604
  self, data: bytes, packet_seq: int = None, raw_packet: bytes = None
517
605
  ) -> None:
518
- """Parse switch event packet which contains multiple message types."""
519
- self._logger.info(
520
- f"Parsing incoming switch event packet #{packet_seq}... Data: {b2a(data)}"
521
- )
522
-
523
- # Special handling for message type 0x29 - likely an extended/aux message
524
- if len(data) >= 1 and data[0] == 0x29:
525
- # Log details so we can correlate with outgoing ExtPacketSend trials
526
- if len(data) >= 3:
527
- length = ((data[2] >> 4) & 15) + 1
528
- parameter = data[2] & 15
529
- payload = data[3 : 3 + min(length, max(0, len(data) - 3))]
530
- self._logger.info(
531
- f"Ext-like message at packet head: flags=0x{data[1]:02x}, param={parameter}, payload={b2a(payload)}"
532
- )
533
- else:
534
- self._logger.info(
535
- f"Ext-like message 0x29 at packet head with insufficient length: {b2a(data)}"
536
- )
537
- return
538
-
539
- pos = 0
540
- oldPos = 0
541
- switch_events_found = 0
542
-
543
- try:
544
- while pos <= len(data) - 3:
545
- oldPos = pos
606
+ """Parse decrypted packet type=7 payload (INVOCATION stream).
546
607
 
547
- # Parse message header
548
- message_type = data[pos]
549
- flags = data[pos + 1]
550
- length = ((data[pos + 2] >> 4) & 15) + 1
551
- parameter = data[pos + 2] # Full byte, not just lower 4 bits
552
- pos += 3
608
+ Ground truth: casambi-android `v1.C1775b.Q(Q2.h)` parses decrypted packet type=7
609
+ as a stream of INVOCATION frames. Switch button events are INVOCATIONs.
610
+ """
553
611
 
554
- # Sanity check: message type should be reasonable
555
- if message_type > 0x80:
556
- self._logger.debug(
557
- f"Skipping invalid message type 0x{message_type:02x} at position {oldPos}"
558
- )
559
- # Try to resync by looking for next valid message
560
- pos = oldPos + 1
561
- continue
562
-
563
- # Check if we have enough data for the payload
564
- if pos + length > len(data):
565
- self._logger.debug(
566
- f"Incomplete message at position {oldPos}. "
567
- f"Type: 0x{message_type:02x}, declared length: {length}, available: {len(data) - pos}"
568
- )
569
- break
570
-
571
- # Extract the payload
572
- payload = data[pos : pos + length]
573
- pos += length
574
-
575
- # Process based on message type
576
- if message_type == 0x08 or message_type == 0x10: # Switch/button events
577
- switch_events_found += 1
578
-
579
- # Button extraction differs between type 0x08 and type 0x10
580
- if message_type == 0x08:
581
- # For type 0x08, the lower nibble is a code that maps to physical button id
582
- # Using formula: ((code + 2) % 4) + 1 based on reverse engineering findings
583
- code_nibble = parameter & 0x0F
584
- button = ((code_nibble + 2) % 4) + 1
585
- self._logger.debug(
586
- f"Type 0x08 button extraction: parameter=0x{parameter:02x}, code={code_nibble}, button={button}"
587
- )
588
- else:
589
- # For type 0x10, use existing logic
590
- button_lower = parameter & 0x0F
591
- button_upper = (parameter >> 4) & 0x0F
592
-
593
- # Use upper 4 bits if lower 4 bits are 0, otherwise use lower 4 bits
594
- if button_lower == 0 and button_upper != 0:
595
- button = button_upper
596
- self._logger.debug(
597
- f"Type 0x10 button extraction: parameter=0x{parameter:02x}, using upper nibble, button={button}"
598
- )
599
- else:
600
- button = button_lower
601
- self._logger.debug(
602
- f"Type 0x10 button extraction: parameter=0x{parameter:02x}, using lower nibble, button={button}"
603
- )
604
-
605
- # For type 0x10 messages, we need to pass additional data beyond the declared payload
606
- if message_type == 0x10:
607
- # Extend to include at least 10 bytes from message start for state byte
608
- extended_end = min(oldPos + 11, len(data))
609
- full_message_data = data[oldPos:extended_end]
610
- else:
611
- full_message_data = data
612
- self._processSwitchMessage(
613
- message_type,
614
- flags,
615
- button,
616
- payload,
617
- full_message_data,
618
- oldPos,
619
- packet_seq,
620
- raw_packet,
621
- )
622
- elif message_type == 0x29:
623
- # Extended/aux message embedded in switch event packet
624
- self._logger.info(
625
- f"Embedded 0x29 ext-like msg: flags=0x{flags:02x}, param=0x{parameter & 0x0F:01x}, payload={b2a(payload)}"
626
- )
627
- elif message_type in [0x00, 0x06, 0x09, 0x1F, 0x2A]:
628
- # Known non-switch message types - log at debug level
629
- self._logger.debug(
630
- f"Non-switch message type 0x{message_type:02x}: flags=0x{flags:02x}, "
631
- f"param={parameter}, payload={b2a(payload)}"
632
- )
633
- else:
634
- # Unknown message types - log at info level
635
- self._logger.info(
636
- f"Unknown message type 0x{message_type:02x}: flags=0x{flags:02x}, "
637
- f"param={parameter}, payload={b2a(payload)}"
638
- )
612
+ self._logger.info(
613
+ "Parsing incoming switch event packet #%s... Data: %s",
614
+ packet_seq,
615
+ b2a(data),
616
+ )
617
+ self._logger.info(
618
+ "[CASAMBI_SWITCH_PACKET] Full data #%s: hex=%s len=%d",
619
+ packet_seq,
620
+ b2a(data),
621
+ len(data),
622
+ )
639
623
 
640
- oldPos = pos
624
+ events, stats = self._switchDecoder.decode(
625
+ data,
626
+ packet_seq=packet_seq,
627
+ raw_packet=raw_packet,
628
+ arrival_sequence=self._inPacketCount,
629
+ )
641
630
 
642
- except IndexError:
643
- self._logger.error(
644
- f"Ran out of data while parsing switch event packet! "
645
- f"Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
646
- )
631
+ self._logger.info(
632
+ "[CASAMBI_SWITCH_SUMMARY] packet=%s frames=%d button_frames=%d input_frames=%d ignored=%d emitted=%d suppressed_same_state=%d",
633
+ packet_seq,
634
+ stats.frames_total,
635
+ stats.frames_button,
636
+ stats.frames_input,
637
+ stats.frames_ignored,
638
+ stats.events_emitted,
639
+ stats.events_suppressed_same_state,
640
+ )
647
641
 
648
- if switch_events_found == 0:
649
- self._logger.debug(f"No switch events found in packet: {b2a(data)}")
642
+ for ev in events:
643
+ # Back-compat alias: older consumers looked for 'flags'
644
+ if "flags" not in ev:
645
+ ev["flags"] = ev.get("invocation_flags")
646
+ self._dataCallback(IncommingPacketType.SwitchEvent, ev)
650
647
 
651
648
  def _processSwitchMessage(
652
649
  self,