python-jebao 0.1.1__tar.gz → 0.1.3__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.
- {python_jebao-0.1.1 → python_jebao-0.1.3}/PKG-INFO +1 -1
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/__init__.py +1 -1
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/protocol.py +121 -64
- {python_jebao-0.1.1 → python_jebao-0.1.3}/pyproject.toml +1 -1
- {python_jebao-0.1.1 → python_jebao-0.1.3}/python_jebao.egg-info/PKG-INFO +1 -1
- {python_jebao-0.1.1 → python_jebao-0.1.3}/LICENSE +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/README.md +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/const.py +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/device.py +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/discovery.py +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/exceptions.py +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/mdp20000.py +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/jebao/retry.py +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/python_jebao.egg-info/SOURCES.txt +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/python_jebao.egg-info/dependency_links.txt +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/python_jebao.egg-info/requires.txt +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/python_jebao.egg-info/top_level.txt +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/setup.cfg +0 -0
- {python_jebao-0.1.1 → python_jebao-0.1.3}/setup.py +0 -0
|
@@ -55,7 +55,10 @@ class JebaoProtocol:
|
|
|
55
55
|
self._sequence = 1
|
|
56
56
|
self._keepalive_task: Optional[asyncio.Task] = None
|
|
57
57
|
self._passcode: Optional[bytes] = None
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
# Locks for thread-safe operation
|
|
60
|
+
self._request_lock = asyncio.Lock() # Serialize entire request-response cycles
|
|
61
|
+
self._read_lock = asyncio.Lock() # Low-level read serialization
|
|
59
62
|
|
|
60
63
|
# Message dispatcher
|
|
61
64
|
self._message_queues: Dict[int, asyncio.Queue] = {}
|
|
@@ -360,35 +363,41 @@ class JebaoProtocol:
|
|
|
360
363
|
JebaoConnectionError: Not connected
|
|
361
364
|
JebaoTimeoutError: Request timed out
|
|
362
365
|
"""
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
366
|
+
# Serialize requests - only one operation at a time
|
|
367
|
+
async with self._request_lock:
|
|
368
|
+
if not self.is_connected:
|
|
369
|
+
raise JebaoConnectionError("Not connected")
|
|
370
|
+
|
|
371
|
+
# Drain any unexpected leftover bytes as safety net (very short timeout)
|
|
372
|
+
# Normally padding is read explicitly, but this catches edge cases
|
|
373
|
+
await self._drain_garbage_bytes(timeout=0.01)
|
|
374
|
+
|
|
375
|
+
# Send simple status request (0x90)
|
|
376
|
+
# Format: 00 00 00 03 04 00 00 90 02
|
|
377
|
+
# The last byte (02) requests values, and response will be message type 91 or 0x00
|
|
378
|
+
request = bytes.fromhex("0000000304000090 02")
|
|
379
|
+
_LOGGER.debug(f"Sending status request: {request.hex()}")
|
|
380
|
+
await self._send_raw(request)
|
|
381
|
+
_LOGGER.debug("Status request sent, waiting for response...")
|
|
382
|
+
|
|
383
|
+
# Read response directly (no dispatcher)
|
|
384
|
+
try:
|
|
385
|
+
response = await asyncio.wait_for(self._read_raw(), timeout=timeout)
|
|
386
|
+
except asyncio.TimeoutError as err:
|
|
387
|
+
raise JebaoTimeoutError("Status request timed out") from err
|
|
379
388
|
|
|
380
|
-
|
|
389
|
+
_LOGGER.debug(f"Status response length: {len(response)}, data: {response.hex()[:100]}...")
|
|
381
390
|
|
|
382
|
-
|
|
391
|
+
# Note: Don't drain here - pump may send unsolicited updates that we shouldn't consume
|
|
383
392
|
|
|
384
|
-
|
|
385
|
-
|
|
393
|
+
if len(response) < 10:
|
|
394
|
+
raise JebaoCommandError(f"Invalid status response: too short ({len(response)} bytes)")
|
|
386
395
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
396
|
+
# Log the message type and first few bytes for debugging
|
|
397
|
+
msg_type = response[7] if len(response) > 7 else 0
|
|
398
|
+
_LOGGER.debug(f"Status response type: 0x{msg_type:02x}, bytes 10-12: {response[10:12].hex() if len(response) > 11 else 'N/A'}")
|
|
390
399
|
|
|
391
|
-
|
|
400
|
+
return response
|
|
392
401
|
|
|
393
402
|
async def send_control_command(
|
|
394
403
|
self,
|
|
@@ -412,48 +421,48 @@ class JebaoProtocol:
|
|
|
412
421
|
JebaoCommandError: Command failed
|
|
413
422
|
JebaoTimeoutError: Command timed out
|
|
414
423
|
"""
|
|
415
|
-
|
|
416
|
-
|
|
424
|
+
# Serialize requests - only one operation at a time
|
|
425
|
+
async with self._request_lock:
|
|
426
|
+
if not self.is_connected:
|
|
427
|
+
raise JebaoConnectionError("Not connected")
|
|
417
428
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
# Read ACK (message type 0x94) directly
|
|
433
|
-
# The pump sends an ACK for control commands
|
|
434
|
-
try:
|
|
435
|
-
ack = await asyncio.wait_for(self._read_raw(), timeout=timeout)
|
|
436
|
-
ack_type = ack[7] if len(ack) > 7 else 0
|
|
437
|
-
_LOGGER.debug("Control command ACK received: type=0x%02x, %d bytes", ack_type, len(ack))
|
|
429
|
+
# Drain any unexpected leftover bytes as safety net (very short timeout)
|
|
430
|
+
# Normally padding is read explicitly, but this catches edge cases
|
|
431
|
+
await self._drain_garbage_bytes(timeout=0.01)
|
|
432
|
+
|
|
433
|
+
command = self._build_control_command(opcode1, opcode2, param1, param2)
|
|
434
|
+
_LOGGER.debug(
|
|
435
|
+
"Sending control command: opcode1=0x%02x, opcode2=0x%02x, param1=%d, param2=%d",
|
|
436
|
+
opcode1,
|
|
437
|
+
opcode2,
|
|
438
|
+
param1,
|
|
439
|
+
param2,
|
|
440
|
+
)
|
|
441
|
+
await self._send_raw(command)
|
|
438
442
|
|
|
439
|
-
#
|
|
440
|
-
#
|
|
443
|
+
# Read ACK (message type 0x94) directly
|
|
444
|
+
# The pump sends an ACK for control commands
|
|
441
445
|
try:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
_LOGGER.
|
|
445
|
-
"Pump sent unsolicited status update after control command: type=0x%02x",
|
|
446
|
-
status_type,
|
|
447
|
-
)
|
|
448
|
-
# Drain any remaining garbage after the status update
|
|
449
|
-
await self._drain_garbage_bytes()
|
|
450
|
-
except asyncio.TimeoutError:
|
|
451
|
-
# No status update sent, that's OK
|
|
452
|
-
_LOGGER.debug("No unsolicited status update after control command")
|
|
446
|
+
ack = await asyncio.wait_for(self._read_raw(), timeout=timeout)
|
|
447
|
+
ack_type = ack[7] if len(ack) > 7 else 0
|
|
448
|
+
_LOGGER.debug("Control command ACK received: type=0x%02x, %d bytes", ack_type, len(ack))
|
|
453
449
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
450
|
+
# After ACK, pump may send unsolicited status update
|
|
451
|
+
# Try to read it with short timeout
|
|
452
|
+
try:
|
|
453
|
+
status_update = await asyncio.wait_for(self._read_raw(), timeout=0.5)
|
|
454
|
+
status_type = status_update[7] if len(status_update) > 7 else 0
|
|
455
|
+
_LOGGER.info(
|
|
456
|
+
"Pump sent unsolicited status update after control command: type=0x%02x",
|
|
457
|
+
status_type,
|
|
458
|
+
)
|
|
459
|
+
except asyncio.TimeoutError:
|
|
460
|
+
# No status update sent, that's OK
|
|
461
|
+
_LOGGER.debug("No unsolicited status update after control command")
|
|
462
|
+
|
|
463
|
+
except asyncio.TimeoutError:
|
|
464
|
+
_LOGGER.warning("Control command sent but no ACK received (pump may still process it)")
|
|
465
|
+
# Don't raise - pump may still process the command even without ACK
|
|
457
466
|
|
|
458
467
|
def _build_control_command(
|
|
459
468
|
self, opcode1: int, opcode2: int, param1: int, param2: int
|
|
@@ -580,6 +589,8 @@ class JebaoProtocol:
|
|
|
580
589
|
self._writer.write(data)
|
|
581
590
|
await self._writer.drain()
|
|
582
591
|
except Exception as err:
|
|
592
|
+
_LOGGER.error("Send failed, closing connection: %s", err)
|
|
593
|
+
await self.disconnect()
|
|
583
594
|
raise JebaoConnectionError(f"Send failed: {err}") from err
|
|
584
595
|
|
|
585
596
|
async def _read_raw(self) -> bytes:
|
|
@@ -606,10 +617,17 @@ class JebaoProtocol:
|
|
|
606
617
|
for attempt in range(max_resync_attempts):
|
|
607
618
|
if header[0:4] == b"\x00\x00\x00\x03":
|
|
608
619
|
# Valid header found
|
|
620
|
+
if attempt > 0:
|
|
621
|
+
# Log when we had to re-sync (shouldn't happen often now)
|
|
622
|
+
_LOGGER.warning(
|
|
623
|
+
"Had to re-sync message stream after %d attempt(s) - "
|
|
624
|
+
"garbage bytes not properly drained",
|
|
625
|
+
attempt
|
|
626
|
+
)
|
|
609
627
|
break
|
|
610
628
|
|
|
611
629
|
# Invalid header - try to re-sync by reading one more byte
|
|
612
|
-
_LOGGER.
|
|
630
|
+
_LOGGER.debug(
|
|
613
631
|
"Invalid header %s, attempting re-sync (attempt %d/%d)",
|
|
614
632
|
header[0:4].hex(),
|
|
615
633
|
attempt + 1,
|
|
@@ -632,12 +650,51 @@ class JebaoProtocol:
|
|
|
632
650
|
|
|
633
651
|
result = header + remaining
|
|
634
652
|
msg_type = result[7] if len(result) > 7 else 0
|
|
653
|
+
|
|
654
|
+
# Only message type 0x00 (extended status responses) has 129-byte padding
|
|
655
|
+
# All other message types have no padding
|
|
656
|
+
if msg_type == MSG_EXTENDED_DATA:
|
|
657
|
+
# Read fixed padding (129 bytes) that pump sends for extended responses
|
|
658
|
+
# The pump uses fixed-frame protocol with padding (0xee or 0x00)
|
|
659
|
+
padding = await self._reader.readexactly(129)
|
|
660
|
+
|
|
661
|
+
# Validate padding - should be mostly 0xee or 0x00 patterns
|
|
662
|
+
# Check first 4 bytes and last byte
|
|
663
|
+
padding_start = padding[0:4]
|
|
664
|
+
padding_end = padding[-1]
|
|
665
|
+
|
|
666
|
+
# Valid patterns: eeeeeeee or 00000000 at start, 00 at end
|
|
667
|
+
is_valid_padding = (
|
|
668
|
+
(padding_start == b'\xee\xee\xee\xee' or padding_start == b'\x00\x00\x00\x00')
|
|
669
|
+
and padding_end == 0x00
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
if not is_valid_padding:
|
|
673
|
+
_LOGGER.error(
|
|
674
|
+
"Invalid padding detected! Start: %s, End: 0x%02x - possible protocol corruption",
|
|
675
|
+
padding_start.hex(),
|
|
676
|
+
padding_end
|
|
677
|
+
)
|
|
678
|
+
# Drain any additional garbage as fallback
|
|
679
|
+
await self._drain_garbage_bytes(timeout=0.05)
|
|
680
|
+
raise JebaoConnectionError(
|
|
681
|
+
f"Protocol error: invalid padding (start={padding_start.hex()}, end=0x{padding_end:02x})"
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
_LOGGER.debug(
|
|
685
|
+
"Read 129-byte padding frame for msg type 0x00 (start: %s, end: 0x%02x)",
|
|
686
|
+
padding_start.hex(),
|
|
687
|
+
padding_end
|
|
688
|
+
)
|
|
689
|
+
|
|
635
690
|
_LOGGER.debug(
|
|
636
691
|
f"Message complete: {len(result)} bytes, type=0x{msg_type:02x}"
|
|
637
692
|
)
|
|
638
693
|
return result
|
|
639
694
|
except asyncio.IncompleteReadError as err:
|
|
640
695
|
_LOGGER.error("IncompleteReadError - connection closed by peer")
|
|
696
|
+
# Mark connection as closed so reconnection can happen
|
|
697
|
+
await self.disconnect()
|
|
641
698
|
raise JebaoConnectionError("Connection closed") from err
|
|
642
699
|
except JebaoConnectionError:
|
|
643
700
|
raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|