python-jebao 0.1.5__tar.gz → 0.1.7__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.5 → python_jebao-0.1.7}/PKG-INFO +1 -1
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/__init__.py +1 -1
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/const.py +4 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/discovery.py +2 -2
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/protocol.py +49 -29
- {python_jebao-0.1.5 → python_jebao-0.1.7}/pyproject.toml +1 -1
- {python_jebao-0.1.5 → python_jebao-0.1.7}/python_jebao.egg-info/PKG-INFO +1 -1
- {python_jebao-0.1.5 → python_jebao-0.1.7}/LICENSE +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/README.md +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/device.py +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/exceptions.py +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/mdp20000.py +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/jebao/retry.py +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/python_jebao.egg-info/SOURCES.txt +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/python_jebao.egg-info/dependency_links.txt +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/python_jebao.egg-info/requires.txt +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/python_jebao.egg-info/top_level.txt +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/setup.cfg +0 -0
- {python_jebao-0.1.5 → python_jebao-0.1.7}/setup.py +0 -0
|
@@ -31,8 +31,12 @@ CONTROL_COMMAND_SIZE = 323
|
|
|
31
31
|
|
|
32
32
|
# Product keys
|
|
33
33
|
PRODUCT_KEY_MDP20000 = "04020B28"
|
|
34
|
+
PRODUCT_KEY_MDP20000_V2 = "04D30Q34" # Newer MDP-20000 hardware revision
|
|
34
35
|
PRODUCT_KEY_MD44 = "04020B27" # Assumed, verify if needed
|
|
35
36
|
|
|
37
|
+
# All known product keys that identify an MDP-20000 (across hardware revisions)
|
|
38
|
+
PRODUCT_KEYS_MDP20000 = frozenset({PRODUCT_KEY_MDP20000, PRODUCT_KEY_MDP20000_V2})
|
|
39
|
+
|
|
36
40
|
# Model identifiers
|
|
37
41
|
MODEL_MDP20000 = "MDP-20000"
|
|
38
42
|
MODEL_MD44 = "MD-4.4"
|
|
@@ -12,7 +12,7 @@ from .const import (
|
|
|
12
12
|
MODEL_MD44,
|
|
13
13
|
MODEL_MDP20000,
|
|
14
14
|
PRODUCT_KEY_MD44,
|
|
15
|
-
|
|
15
|
+
PRODUCT_KEYS_MDP20000,
|
|
16
16
|
UDP_DISCOVERY_PORT,
|
|
17
17
|
UDP_LISTEN_PORT,
|
|
18
18
|
)
|
|
@@ -357,7 +357,7 @@ class JebaoDiscovery:
|
|
|
357
357
|
product_key = data[product_key_start : product_key_start + 8].rstrip(b"\x00").decode("ascii")
|
|
358
358
|
|
|
359
359
|
# Determine model from product key
|
|
360
|
-
if product_key
|
|
360
|
+
if product_key in PRODUCT_KEYS_MDP20000:
|
|
361
361
|
model = MODEL_MDP20000
|
|
362
362
|
elif product_key == PRODUCT_KEY_MD44:
|
|
363
363
|
model = MODEL_MD44
|
|
@@ -530,16 +530,22 @@ class JebaoProtocol:
|
|
|
530
530
|
return bytes(buffer)
|
|
531
531
|
|
|
532
532
|
async def _drain_garbage_bytes(self, timeout: float = 0.1) -> None:
|
|
533
|
-
"""Drain
|
|
533
|
+
"""Drain leftover bytes and unsolicited messages from the socket buffer.
|
|
534
534
|
|
|
535
|
-
The MDP-20000 pump
|
|
536
|
-
|
|
537
|
-
|
|
535
|
+
The MDP-20000 pump emits two things we need to consume before a request:
|
|
536
|
+
- Padding bytes (0xee / 0x00) trailing some responses
|
|
537
|
+
- Unsolicited type 0x00 status broadcasts (header + body + 129-byte padding)
|
|
538
|
+
|
|
539
|
+
If an unsolicited type 0x00 frame is found and we only consume its body,
|
|
540
|
+
the trailing 129-byte padding remains in the buffer and corrupts the next
|
|
541
|
+
read (forcing a slow byte-by-byte resync that often races the pump's
|
|
542
|
+
connection close, surfacing as IncompleteReadError).
|
|
538
543
|
|
|
539
544
|
Args:
|
|
540
|
-
timeout: How long to wait for
|
|
545
|
+
timeout: How long to wait for bytes (default 0.1s)
|
|
541
546
|
"""
|
|
542
|
-
|
|
547
|
+
reader = self._reader
|
|
548
|
+
if reader is None:
|
|
543
549
|
return
|
|
544
550
|
|
|
545
551
|
try:
|
|
@@ -548,7 +554,7 @@ class JebaoProtocol:
|
|
|
548
554
|
try:
|
|
549
555
|
# Try to read header of next message with short timeout
|
|
550
556
|
header = await asyncio.wait_for(
|
|
551
|
-
|
|
557
|
+
reader.read(5), timeout=timeout
|
|
552
558
|
)
|
|
553
559
|
if not header:
|
|
554
560
|
# No more data
|
|
@@ -559,20 +565,21 @@ class JebaoProtocol:
|
|
|
559
565
|
continue
|
|
560
566
|
# Check if it's a valid header
|
|
561
567
|
if header[0:4] == b"\x00\x00\x00\x03":
|
|
562
|
-
#
|
|
563
|
-
#
|
|
564
|
-
# This is a problem - we've consumed a valid message header
|
|
565
|
-
_LOGGER.warning(
|
|
566
|
-
"Found valid header while draining - this shouldn't happen! "
|
|
567
|
-
"Pump may have sent unsolicited message."
|
|
568
|
-
)
|
|
569
|
-
# Read the rest of this message to consume it
|
|
568
|
+
# Pump sent an unsolicited message (e.g. periodic status broadcast).
|
|
569
|
+
# Consume the full frame so we don't desync the stream.
|
|
570
570
|
length = header[4]
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
571
|
+
body = await reader.readexactly(length)
|
|
572
|
+
msg_type = body[2] if len(body) > 2 else 0
|
|
573
|
+
total = len(header) + len(body)
|
|
574
|
+
# Type 0x00 messages carry an extra 129-byte padding frame
|
|
575
|
+
# (same as _read_raw). Skip it or stale bytes corrupt the next read.
|
|
576
|
+
if msg_type == MSG_EXTENDED_DATA:
|
|
577
|
+
padding = await reader.readexactly(129)
|
|
578
|
+
total += len(padding)
|
|
579
|
+
_LOGGER.debug(
|
|
580
|
+
"Drained unsolicited message: type=0x%02x, %d bytes total",
|
|
581
|
+
msg_type,
|
|
582
|
+
total,
|
|
576
583
|
)
|
|
577
584
|
else:
|
|
578
585
|
# Garbage header - log and continue draining
|
|
@@ -612,15 +619,21 @@ class JebaoProtocol:
|
|
|
612
619
|
Raises:
|
|
613
620
|
JebaoConnectionError: Read failed
|
|
614
621
|
"""
|
|
615
|
-
if not self._reader:
|
|
616
|
-
raise JebaoConnectionError("Not connected")
|
|
617
|
-
|
|
618
622
|
async with self._read_lock:
|
|
623
|
+
# Bind the reader locally under the lock. disconnect() (called from
|
|
624
|
+
# our own IncompleteReadError handler, from _send_raw's error path,
|
|
625
|
+
# and from the coordinator) sets self._reader = None from another
|
|
626
|
+
# task; re-reading self._reader across the awaits below would raise
|
|
627
|
+
# AttributeError. Reading from a captured, closed StreamReader raises
|
|
628
|
+
# IncompleteReadError/ConnectionResetError instead, which we handle.
|
|
629
|
+
reader = self._reader
|
|
630
|
+
if reader is None:
|
|
631
|
+
raise JebaoConnectionError("Not connected")
|
|
619
632
|
try:
|
|
620
633
|
# Read header (4 bytes) + length byte (1 byte) = 5 bytes total
|
|
621
634
|
# Format: 00 00 00 03 [LENGTH]
|
|
622
635
|
_LOGGER.debug("Reading next message header...")
|
|
623
|
-
header = await
|
|
636
|
+
header = await reader.readexactly(5)
|
|
624
637
|
|
|
625
638
|
# Validate header - if invalid, try to re-sync
|
|
626
639
|
max_resync_attempts = 50
|
|
@@ -644,7 +657,7 @@ class JebaoProtocol:
|
|
|
644
657
|
max_resync_attempts,
|
|
645
658
|
)
|
|
646
659
|
# Shift header left by 1 byte and read 1 new byte
|
|
647
|
-
next_byte = await
|
|
660
|
+
next_byte = await reader.readexactly(1)
|
|
648
661
|
header = header[1:] + next_byte
|
|
649
662
|
else:
|
|
650
663
|
# Failed to re-sync after max attempts
|
|
@@ -656,7 +669,7 @@ class JebaoProtocol:
|
|
|
656
669
|
_LOGGER.debug(f"Valid header found, reading {length} more bytes")
|
|
657
670
|
|
|
658
671
|
# Read the remaining bytes based on length field
|
|
659
|
-
remaining = await
|
|
672
|
+
remaining = await reader.readexactly(length)
|
|
660
673
|
|
|
661
674
|
result = header + remaining
|
|
662
675
|
msg_type = result[7] if len(result) > 7 else 0
|
|
@@ -666,7 +679,7 @@ class JebaoProtocol:
|
|
|
666
679
|
if msg_type == MSG_EXTENDED_DATA:
|
|
667
680
|
# Read fixed padding (129 bytes) that pump sends for extended responses
|
|
668
681
|
# The pump uses fixed-frame protocol with padding (0xee or 0x00)
|
|
669
|
-
padding = await
|
|
682
|
+
padding = await reader.readexactly(129)
|
|
670
683
|
|
|
671
684
|
# Validate padding - should be mostly 0xee or 0x00 patterns
|
|
672
685
|
# Check first 4 bytes and last byte
|
|
@@ -701,8 +714,15 @@ class JebaoProtocol:
|
|
|
701
714
|
f"Message complete: {len(result)} bytes, type=0x{msg_type:02x}"
|
|
702
715
|
)
|
|
703
716
|
return result
|
|
704
|
-
except asyncio.IncompleteReadError as err:
|
|
705
|
-
|
|
717
|
+
except (asyncio.IncompleteReadError, ConnectionResetError) as err:
|
|
718
|
+
# Expected when the pump drops an idle connection. The reconnect
|
|
719
|
+
# is logged by the coordinator/retry layer, so keep this at debug
|
|
720
|
+
# to avoid flooding the log every poll.
|
|
721
|
+
_LOGGER.debug(
|
|
722
|
+
"%s Connection closed by peer (%s) - will reconnect",
|
|
723
|
+
self.device_identifier,
|
|
724
|
+
type(err).__name__,
|
|
725
|
+
)
|
|
706
726
|
# Mark connection as closed so reconnection can happen
|
|
707
727
|
await self.disconnect()
|
|
708
728
|
raise JebaoConnectionError("Connection closed") from err
|
|
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
|