python-jebao 0.1.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jebao
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Python library for controlling Jebao aquarium pumps
5
5
  Home-page: https://github.com/jrigling/python-jebao
6
6
  Author: Justin Rigling
@@ -16,7 +16,7 @@ from .const import (
16
16
  MODEL_MD44,
17
17
  )
18
18
 
19
- __version__ = "0.1.6"
19
+ __version__ = "0.1.7"
20
20
 
21
21
  __all__ = [
22
22
  "JebaoDevice",
@@ -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
- PRODUCT_KEY_MDP20000,
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 == PRODUCT_KEY_MDP20000:
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
@@ -544,7 +544,8 @@ class JebaoProtocol:
544
544
  Args:
545
545
  timeout: How long to wait for bytes (default 0.1s)
546
546
  """
547
- if not self._reader:
547
+ reader = self._reader
548
+ if reader is None:
548
549
  return
549
550
 
550
551
  try:
@@ -553,7 +554,7 @@ class JebaoProtocol:
553
554
  try:
554
555
  # Try to read header of next message with short timeout
555
556
  header = await asyncio.wait_for(
556
- self._reader.read(5), timeout=timeout
557
+ reader.read(5), timeout=timeout
557
558
  )
558
559
  if not header:
559
560
  # No more data
@@ -567,13 +568,13 @@ class JebaoProtocol:
567
568
  # Pump sent an unsolicited message (e.g. periodic status broadcast).
568
569
  # Consume the full frame so we don't desync the stream.
569
570
  length = header[4]
570
- body = await self._reader.readexactly(length)
571
+ body = await reader.readexactly(length)
571
572
  msg_type = body[2] if len(body) > 2 else 0
572
573
  total = len(header) + len(body)
573
574
  # Type 0x00 messages carry an extra 129-byte padding frame
574
575
  # (same as _read_raw). Skip it or stale bytes corrupt the next read.
575
576
  if msg_type == MSG_EXTENDED_DATA:
576
- padding = await self._reader.readexactly(129)
577
+ padding = await reader.readexactly(129)
577
578
  total += len(padding)
578
579
  _LOGGER.debug(
579
580
  "Drained unsolicited message: type=0x%02x, %d bytes total",
@@ -618,15 +619,21 @@ class JebaoProtocol:
618
619
  Raises:
619
620
  JebaoConnectionError: Read failed
620
621
  """
621
- if not self._reader:
622
- raise JebaoConnectionError("Not connected")
623
-
624
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")
625
632
  try:
626
633
  # Read header (4 bytes) + length byte (1 byte) = 5 bytes total
627
634
  # Format: 00 00 00 03 [LENGTH]
628
635
  _LOGGER.debug("Reading next message header...")
629
- header = await self._reader.readexactly(5)
636
+ header = await reader.readexactly(5)
630
637
 
631
638
  # Validate header - if invalid, try to re-sync
632
639
  max_resync_attempts = 50
@@ -650,7 +657,7 @@ class JebaoProtocol:
650
657
  max_resync_attempts,
651
658
  )
652
659
  # Shift header left by 1 byte and read 1 new byte
653
- next_byte = await self._reader.readexactly(1)
660
+ next_byte = await reader.readexactly(1)
654
661
  header = header[1:] + next_byte
655
662
  else:
656
663
  # Failed to re-sync after max attempts
@@ -662,7 +669,7 @@ class JebaoProtocol:
662
669
  _LOGGER.debug(f"Valid header found, reading {length} more bytes")
663
670
 
664
671
  # Read the remaining bytes based on length field
665
- remaining = await self._reader.readexactly(length)
672
+ remaining = await reader.readexactly(length)
666
673
 
667
674
  result = header + remaining
668
675
  msg_type = result[7] if len(result) > 7 else 0
@@ -672,7 +679,7 @@ class JebaoProtocol:
672
679
  if msg_type == MSG_EXTENDED_DATA:
673
680
  # Read fixed padding (129 bytes) that pump sends for extended responses
674
681
  # The pump uses fixed-frame protocol with padding (0xee or 0x00)
675
- padding = await self._reader.readexactly(129)
682
+ padding = await reader.readexactly(129)
676
683
 
677
684
  # Validate padding - should be mostly 0xee or 0x00 patterns
678
685
  # Check first 4 bytes and last byte
@@ -707,8 +714,15 @@ class JebaoProtocol:
707
714
  f"Message complete: {len(result)} bytes, type=0x{msg_type:02x}"
708
715
  )
709
716
  return result
710
- except asyncio.IncompleteReadError as err:
711
- _LOGGER.error("%s IncompleteReadError - connection closed by peer", self.device_identifier)
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
+ )
712
726
  # Mark connection as closed so reconnection can happen
713
727
  await self.disconnect()
714
728
  raise JebaoConnectionError("Connection closed") from err
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-jebao"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Python library for controlling Jebao aquarium pumps"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jebao
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Python library for controlling Jebao aquarium pumps
5
5
  Home-page: https://github.com/jrigling/python-jebao
6
6
  Author: Justin Rigling
File without changes
File without changes
File without changes
File without changes