casambi-bt-revamped 0.3.4__py3-none-any.whl → 0.3.6__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/_client.py +86 -13
- CasambiBt/_client_android_parser.py +215 -0
- {casambi_bt_revamped-0.3.4.dist-info → casambi_bt_revamped-0.3.6.dist-info}/METADATA +1 -1
- {casambi_bt_revamped-0.3.4.dist-info → casambi_bt_revamped-0.3.6.dist-info}/RECORD +7 -6
- {casambi_bt_revamped-0.3.4.dist-info → casambi_bt_revamped-0.3.6.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.4.dist-info → casambi_bt_revamped-0.3.6.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.4.dist-info → casambi_bt_revamped-0.3.6.dist-info}/top_level.txt +0 -0
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):
|
|
@@ -415,23 +421,35 @@ class CasambiClient:
|
|
|
415
421
|
# TODO: Check incoming counter and direction flag
|
|
416
422
|
self._inPacketCount += 1
|
|
417
423
|
|
|
418
|
-
# Store raw encrypted packet for
|
|
419
|
-
|
|
424
|
+
# Store raw encrypted packet for Android parser analysis
|
|
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
|
-
|
|
433
|
+
decrypted_data = self._encryptor.decryptAndVerify(data, data[:4] + self._nonce[4:])
|
|
423
434
|
except InvalidSignature:
|
|
424
435
|
# We only drop packets with invalid signature here instead of going into an error state
|
|
425
436
|
self._logger.error(f"Invalid signature for packet {b2a(data)}!")
|
|
426
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
|
|
427
445
|
|
|
428
|
-
packetType =
|
|
429
|
-
self._logger.debug(f"Incoming data of type {packetType}: {b2a(
|
|
446
|
+
packetType = decrypted_data[0]
|
|
447
|
+
self._logger.debug(f"Incoming data of type {packetType}: {b2a(decrypted_data)}")
|
|
430
448
|
|
|
431
449
|
if packetType == IncommingPacketType.UnitState:
|
|
432
|
-
self._parseUnitStates(
|
|
450
|
+
self._parseUnitStates(decrypted_data[1:])
|
|
433
451
|
elif packetType == IncommingPacketType.SwitchEvent:
|
|
434
|
-
self._parseSwitchEvent(
|
|
452
|
+
self._parseSwitchEvent(decrypted_data[1:], self._inPacketCount, raw_encrypted_packet, android_switch_event)
|
|
435
453
|
elif packetType == IncommingPacketType.NetworkConfig:
|
|
436
454
|
# We don't care about the config the network thinks it has.
|
|
437
455
|
# We assume that cloud config and local config match.
|
|
@@ -485,12 +503,19 @@ class CasambiClient:
|
|
|
485
503
|
f"Ran out of data while parsing unit state! Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
|
|
486
504
|
)
|
|
487
505
|
|
|
488
|
-
def _parseSwitchEvent(self, data: bytes, packet_seq: int = None, raw_packet: bytes = None) -> None:
|
|
506
|
+
def _parseSwitchEvent(self, data: bytes, packet_seq: int = None, raw_packet: bytes = None, android_switch_event: dict = None) -> None:
|
|
489
507
|
"""Parse switch event packet which contains multiple message types"""
|
|
490
508
|
self._logger.info(f"Parsing incoming switch event packet... Data: {b2a(data)}")
|
|
509
|
+
|
|
510
|
+
# Special handling for message type 0x29 - not a switch event
|
|
511
|
+
if len(data) >= 1 and data[0] == 0x29:
|
|
512
|
+
self._logger.debug(f"Ignoring message type 0x29 (not a switch event): {b2a(data)}")
|
|
513
|
+
return
|
|
491
514
|
|
|
492
515
|
pos = 0
|
|
493
516
|
oldPos = 0
|
|
517
|
+
switch_events_found = 0
|
|
518
|
+
|
|
494
519
|
try:
|
|
495
520
|
while pos <= len(data) - 3:
|
|
496
521
|
oldPos = pos
|
|
@@ -502,6 +527,15 @@ class CasambiClient:
|
|
|
502
527
|
parameter = data[pos + 2] & 15
|
|
503
528
|
pos += 3
|
|
504
529
|
|
|
530
|
+
# Sanity check: message type should be reasonable
|
|
531
|
+
if message_type > 0x80:
|
|
532
|
+
self._logger.debug(
|
|
533
|
+
f"Skipping invalid message type 0x{message_type:02x} at position {oldPos}"
|
|
534
|
+
)
|
|
535
|
+
# Try to resync by looking for next valid message
|
|
536
|
+
pos = oldPos + 1
|
|
537
|
+
continue
|
|
538
|
+
|
|
505
539
|
# Check if we have enough data for the payload
|
|
506
540
|
if pos + length > len(data):
|
|
507
541
|
self._logger.debug(
|
|
@@ -516,11 +550,21 @@ class CasambiClient:
|
|
|
516
550
|
|
|
517
551
|
# Process based on message type
|
|
518
552
|
if message_type == 0x08 or message_type == 0x10: # Switch/button events
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
553
|
+
switch_events_found += 1
|
|
554
|
+
self._processSwitchMessage(message_type, flags, parameter, payload, data, oldPos, packet_seq, raw_packet, android_switch_event)
|
|
555
|
+
elif message_type == 0x29:
|
|
556
|
+
# This shouldn't happen due to check above, but just in case
|
|
557
|
+
self._logger.debug(f"Ignoring embedded type 0x29 message")
|
|
558
|
+
elif message_type in [0x00, 0x06, 0x09, 0x1f, 0x2a]:
|
|
559
|
+
# Known non-switch message types - log at debug level
|
|
522
560
|
self._logger.debug(
|
|
523
|
-
f"
|
|
561
|
+
f"Non-switch message type 0x{message_type:02x}: flags=0x{flags:02x}, "
|
|
562
|
+
f"param={parameter}, payload={b2a(payload)}"
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
# Unknown message types - log at info level
|
|
566
|
+
self._logger.info(
|
|
567
|
+
f"Unknown message type 0x{message_type:02x}: flags=0x{flags:02x}, "
|
|
524
568
|
f"param={parameter}, payload={b2a(payload)}"
|
|
525
569
|
)
|
|
526
570
|
|
|
@@ -531,8 +575,11 @@ class CasambiClient:
|
|
|
531
575
|
f"Ran out of data while parsing switch event packet! "
|
|
532
576
|
f"Remaining data {b2a(data[oldPos:])} in {b2a(data)}."
|
|
533
577
|
)
|
|
578
|
+
|
|
579
|
+
if switch_events_found == 0:
|
|
580
|
+
self._logger.debug(f"No switch events found in packet: {b2a(data)}")
|
|
534
581
|
|
|
535
|
-
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) -> None:
|
|
582
|
+
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:
|
|
536
583
|
"""Process a switch/button message (types 0x08 or 0x10)"""
|
|
537
584
|
if not payload:
|
|
538
585
|
self._logger.error("Switch message has empty payload")
|
|
@@ -547,6 +594,14 @@ class CasambiClient:
|
|
|
547
594
|
extra_data = b''
|
|
548
595
|
if len(payload) > 2:
|
|
549
596
|
extra_data = payload[2:]
|
|
597
|
+
|
|
598
|
+
# Validate extra data for type 0x10
|
|
599
|
+
if message_type == 0x10 and len(extra_data) >= 3:
|
|
600
|
+
# Expected pattern: [unit_id_echo][0x12 or similar][0x00]
|
|
601
|
+
if extra_data[0] != unit_id:
|
|
602
|
+
self._logger.warning(
|
|
603
|
+
f"Extra data validation failed: unit_id_echo {extra_data[0]} != unit_id {unit_id}"
|
|
604
|
+
)
|
|
550
605
|
|
|
551
606
|
event_string = "unknown"
|
|
552
607
|
|
|
@@ -581,6 +636,23 @@ class CasambiClient:
|
|
|
581
636
|
f"Switch event (type 0x{message_type:02x}): button={button}, unit_id={unit_id}, "
|
|
582
637
|
f"action={action_display} ({event_string}), flags=0x{flags:02x}"
|
|
583
638
|
)
|
|
639
|
+
|
|
640
|
+
# Include Android parser comparison if available
|
|
641
|
+
android_comparison = None
|
|
642
|
+
if android_switch_event:
|
|
643
|
+
android_comparison = {
|
|
644
|
+
'unit_id': android_switch_event['unit_id'],
|
|
645
|
+
'button': android_switch_event['button'],
|
|
646
|
+
'state': android_switch_event['state'],
|
|
647
|
+
'param_p': android_switch_event['param_p'],
|
|
648
|
+
'param_s': android_switch_event['param_s'],
|
|
649
|
+
'android_log': android_switch_event['android_log']
|
|
650
|
+
}
|
|
651
|
+
# Log differences
|
|
652
|
+
if android_switch_event['unit_id'] != unit_id:
|
|
653
|
+
self._logger.warning(f"Unit ID mismatch: current={unit_id}, android={android_switch_event['unit_id']}")
|
|
654
|
+
if android_switch_event['button'] != button:
|
|
655
|
+
self._logger.warning(f"Button mismatch: current={button}, android={android_switch_event['button']}")
|
|
584
656
|
|
|
585
657
|
self._dataCallback(
|
|
586
658
|
IncommingPacketType.SwitchEvent,
|
|
@@ -597,6 +669,7 @@ class CasambiClient:
|
|
|
597
669
|
"decrypted_data": b2a(full_data),
|
|
598
670
|
"message_position": start_pos,
|
|
599
671
|
"payload_hex": b2a(payload),
|
|
672
|
+
"android_comparison": android_comparison,
|
|
600
673
|
},
|
|
601
674
|
)
|
|
602
675
|
|
|
@@ -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
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
CasambiBt/__init__.py,sha256=TW445xSu5PV3TyMjJfwaA1JoWvQQ8LXhZgGdDTfWf3s,302
|
|
2
2
|
CasambiBt/_cache.py,sha256=KZ2xbiHAHXUPa8Gw_75Nw9NL4QSY_sTWHbyYXYUDaB0,3865
|
|
3
3
|
CasambiBt/_casambi.py,sha256=gLLkhEcObgapqTx5Mk7WRClyG29UyfZYZCCIhhOg4H4,23101
|
|
4
|
-
CasambiBt/_client.py,sha256=
|
|
4
|
+
CasambiBt/_client.py,sha256=sYK0PpgzhqRc164Q6E2miPK0pNtQmPLoXiZ69-Wxgxk,27667
|
|
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
|
|
@@ -11,8 +12,8 @@ CasambiBt/_operation.py,sha256=-BuC1Bvtg-G-zSN_b_0JMvXdHZaR6LbTw0S425jg96c,842
|
|
|
11
12
|
CasambiBt/_unit.py,sha256=YiQWoHmMDWHHo4XAjtW8rHsBqIqpmp9MVdv1Mbu2xw4,17043
|
|
12
13
|
CasambiBt/errors.py,sha256=0JgDjaKlAKDes0poWzA8nrTUYQ8qdNfBb8dfaqqzCRA,1664
|
|
13
14
|
CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
casambi_bt_revamped-0.3.
|
|
15
|
-
casambi_bt_revamped-0.3.
|
|
16
|
-
casambi_bt_revamped-0.3.
|
|
17
|
-
casambi_bt_revamped-0.3.
|
|
18
|
-
casambi_bt_revamped-0.3.
|
|
15
|
+
casambi_bt_revamped-0.3.6.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
16
|
+
casambi_bt_revamped-0.3.6.dist-info/METADATA,sha256=iUl6SlcgXC16rJxAiI5mFBfmlQohx-so3y5e_0pa5JQ,3044
|
|
17
|
+
casambi_bt_revamped-0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
casambi_bt_revamped-0.3.6.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
19
|
+
casambi_bt_revamped-0.3.6.dist-info/RECORD,,
|
|
File without changes
|
{casambi_bt_revamped-0.3.4.dist-info → casambi_bt_revamped-0.3.6.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|