python-jebao 0.1.3__tar.gz → 0.1.5__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.3
3
+ Version: 0.1.5
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.3"
19
+ __version__ = "0.1.5"
20
20
 
21
21
  __all__ = [
22
22
  "JebaoDevice",
@@ -34,11 +34,18 @@ class JebaoDevice:
34
34
  self.device_id = device_id
35
35
  self.model = model
36
36
 
37
- self._protocol = JebaoProtocol(host, port)
37
+ self._protocol = JebaoProtocol(host, port, device_id=device_id)
38
38
  self._state: Optional[DeviceState] = None
39
39
  self._speed: Optional[int] = None
40
40
  self._last_update: Optional[float] = None
41
41
 
42
+ @property
43
+ def device_identifier(self) -> str:
44
+ """Get device identifier for logging."""
45
+ if self.device_id:
46
+ return f"[{self.device_id}]"
47
+ return f"[{self.host}]"
48
+
42
49
  @property
43
50
  def is_connected(self) -> bool:
44
51
  """Check if device is connected."""
@@ -90,7 +97,7 @@ class JebaoDevice:
90
97
  JebaoAuthenticationError: Authentication failed
91
98
  """
92
99
  await self._protocol.connect(timeout)
93
- _LOGGER.info("Connected to %s (%s)", self.host, self.model or "Unknown")
100
+ _LOGGER.info("%s Connected (%s)", self.device_identifier, self.model or "Unknown")
94
101
 
95
102
  # Get initial status
96
103
  await self.update()
@@ -98,7 +105,7 @@ class JebaoDevice:
98
105
  async def disconnect(self) -> None:
99
106
  """Disconnect from device."""
100
107
  await self._protocol.disconnect()
101
- _LOGGER.info("Disconnected from %s", self.host)
108
+ _LOGGER.info("%s Disconnected", self.device_identifier)
102
109
 
103
110
  @async_retry(max_attempts=3, delay=0.5)
104
111
  async def update(self, timeout: float = DEFAULT_TIMEOUT) -> None:
@@ -124,12 +131,13 @@ class JebaoDevice:
124
131
  self._speed = status["speed"]
125
132
  self._last_update = time.time()
126
133
  _LOGGER.debug(
127
- "Status updated: state=%s, speed=%d%%",
134
+ "%s Status updated: state=%s, speed=%d%%",
135
+ self.device_identifier,
128
136
  self._state.name,
129
137
  self._speed,
130
138
  )
131
139
  else:
132
- _LOGGER.warning("Failed to parse status response")
140
+ _LOGGER.warning("%s Failed to parse status response", self.device_identifier)
133
141
 
134
142
  async def __aenter__(self):
135
143
  """Async context manager entry."""
@@ -90,7 +90,7 @@ class MDP20000Device(JebaoDevice):
90
90
  This method automatically retries up to 3 times on transient
91
91
  failures (garbage bytes, connection issues).
92
92
  """
93
- _LOGGER.info("Turning on pump")
93
+ _LOGGER.info("%s Turning on pump", self.device_identifier)
94
94
 
95
95
  await self._protocol.send_control_command(
96
96
  CommandOpcode.TURN_ON_OFF,
@@ -115,7 +115,7 @@ class MDP20000Device(JebaoDevice):
115
115
  This method automatically retries up to 3 times on transient
116
116
  failures (garbage bytes, connection issues).
117
117
  """
118
- _LOGGER.info("Turning off pump")
118
+ _LOGGER.info("%s Turning off pump", self.device_identifier)
119
119
 
120
120
  await self._protocol.send_control_command(
121
121
  CommandOpcode.TURN_ON_OFF,
@@ -148,7 +148,7 @@ class MDP20000Device(JebaoDevice):
148
148
  f"Speed must be between {MIN_SPEED} and {MAX_SPEED}, got {percentage}"
149
149
  )
150
150
 
151
- _LOGGER.info("Setting speed to %d%%", percentage)
151
+ _LOGGER.info("%s Setting speed to %d%%", self.device_identifier, percentage)
152
152
 
153
153
  await self._protocol.send_control_command(
154
154
  CommandOpcode.SET_SPEED,
@@ -41,15 +41,17 @@ MSG_EXTENDED_DATA = 0x00
41
41
  class JebaoProtocol:
42
42
  """Low-level protocol implementation for Jebao devices."""
43
43
 
44
- def __init__(self, host: str, port: int = 12416):
44
+ def __init__(self, host: str, port: int = 12416, device_id: Optional[str] = None):
45
45
  """Initialize protocol handler.
46
46
 
47
47
  Args:
48
48
  host: Device IP address
49
49
  port: TCP port (default 12416)
50
+ device_id: Device ID for logging (optional)
50
51
  """
51
52
  self.host = host
52
53
  self.port = port
54
+ self.device_id = device_id
53
55
  self._reader: Optional[asyncio.StreamReader] = None
54
56
  self._writer: Optional[asyncio.StreamWriter] = None
55
57
  self._sequence = 1
@@ -68,6 +70,13 @@ class JebaoProtocol:
68
70
  # Read buffer for handling garbage bytes
69
71
  self._read_buffer = bytearray()
70
72
 
73
+ @property
74
+ def device_identifier(self) -> str:
75
+ """Get device identifier for logging."""
76
+ if self.device_id:
77
+ return f"[{self.device_id}]"
78
+ return f"[{self.host}]"
79
+
71
80
  @property
72
81
  def is_connected(self) -> bool:
73
82
  """Check if connection is established."""
@@ -85,7 +94,7 @@ class JebaoProtocol:
85
94
  JebaoTimeoutError: Operation timed out
86
95
  """
87
96
  try:
88
- _LOGGER.debug("Connecting to %s:%d", self.host, self.port)
97
+ _LOGGER.debug("%s Connecting to %s:%d", self.device_identifier, self.host, self.port)
89
98
  self._reader, self._writer = await asyncio.wait_for(
90
99
  asyncio.open_connection(self.host, self.port), timeout=timeout
91
100
  )
@@ -117,7 +126,7 @@ class JebaoProtocol:
117
126
  Raises:
118
127
  JebaoAuthenticationError: Authentication failed
119
128
  """
120
- _LOGGER.debug("Starting authentication")
129
+ _LOGGER.debug("%s Starting authentication", self.device_identifier)
121
130
 
122
131
  # Step 1: Request passcode
123
132
  request = self._build_passcode_request()
@@ -161,7 +170,7 @@ class JebaoProtocol:
161
170
  _LOGGER.error(f"Login failed - expected type 0x{MSG_LOGIN_SUCCESS:02x} at byte 7, got 0x{response[7]:02x}")
162
171
  raise JebaoAuthenticationError("Login failed")
163
172
 
164
- _LOGGER.info("Authentication successful for %s", self.host)
173
+ _LOGGER.info("%s Authentication successful", self.device_identifier)
165
174
 
166
175
  async def disconnect(self) -> None:
167
176
  """Disconnect from device."""
@@ -188,7 +197,7 @@ class JebaoProtocol:
188
197
  self._writer = None
189
198
  self._reader = None
190
199
 
191
- _LOGGER.debug("Disconnected from %s", self.host)
200
+ _LOGGER.debug("%s Disconnected", self.device_identifier)
192
201
 
193
202
  def _start_keepalive(self) -> None:
194
203
  """Start keepalive task."""
@@ -432,7 +441,8 @@ class JebaoProtocol:
432
441
 
433
442
  command = self._build_control_command(opcode1, opcode2, param1, param2)
434
443
  _LOGGER.debug(
435
- "Sending control command: opcode1=0x%02x, opcode2=0x%02x, param1=%d, param2=%d",
444
+ "%s Sending control command: opcode1=0x%02x, opcode2=0x%02x, param1=%d, param2=%d",
445
+ self.device_identifier,
436
446
  opcode1,
437
447
  opcode2,
438
448
  param1,
@@ -692,7 +702,7 @@ class JebaoProtocol:
692
702
  )
693
703
  return result
694
704
  except asyncio.IncompleteReadError as err:
695
- _LOGGER.error("IncompleteReadError - connection closed by peer")
705
+ _LOGGER.error("%s IncompleteReadError - connection closed by peer", self.device_identifier)
696
706
  # Mark connection as closed so reconnection can happen
697
707
  await self.disconnect()
698
708
  raise JebaoConnectionError("Connection closed") from err
@@ -41,27 +41,57 @@ def async_retry(
41
41
  return await func(*args, **kwargs)
42
42
  except exceptions as e:
43
43
  if attempt == max_attempts:
44
+ device_id = ""
45
+ if args and hasattr(args[0], 'device_identifier'):
46
+ device_id = args[0].device_identifier + " "
44
47
  _LOGGER.error(
45
- f"{func.__name__} failed after {max_attempts} attempts: {e}"
48
+ f"{device_id}{func.__name__} failed after {max_attempts} attempts: {e}"
46
49
  )
47
50
  raise
48
51
 
49
- # Check if error message indicates garbage issue
52
+ # Check if error message indicates specific issues
50
53
  error_msg = str(e).lower()
51
54
  is_garbage_issue = (
52
55
  "synchronization" in error_msg
53
56
  or "garbage" in error_msg
54
57
  or "invalid header" in error_msg
55
58
  )
59
+ is_not_connected = "not connected" in error_msg or "connection closed" in error_msg
60
+
61
+ # Attempt reconnection if disconnected
62
+ if is_not_connected and args and hasattr(args[0], 'connect'):
63
+ device = args[0]
64
+ # Check if device has is_connected property
65
+ if hasattr(device, 'is_connected') and not device.is_connected:
66
+ device_id = device.device_identifier if hasattr(device, 'device_identifier') else ""
67
+ _LOGGER.info(
68
+ f"{device_id} {func.__name__} attempt {attempt}/{max_attempts}: "
69
+ f"Connection lost, attempting to reconnect..."
70
+ )
71
+ try:
72
+ await device.connect()
73
+ _LOGGER.info(f"{device_id} Reconnected successfully, retrying operation")
74
+ # Don't sleep after successful reconnection - retry immediately
75
+ attempt += 1
76
+ continue
77
+ except Exception as reconnect_err:
78
+ _LOGGER.warning(
79
+ f"{device_id} Reconnection failed: {reconnect_err}, "
80
+ f"will retry in {current_delay}s"
81
+ )
82
+
83
+ device_id = ""
84
+ if args and hasattr(args[0], 'device_identifier'):
85
+ device_id = args[0].device_identifier + " "
56
86
 
57
87
  if is_garbage_issue:
58
88
  _LOGGER.warning(
59
- f"{func.__name__} attempt {attempt}/{max_attempts} failed "
89
+ f"{device_id}{func.__name__} attempt {attempt}/{max_attempts} failed "
60
90
  f"due to garbage bytes, retrying in {current_delay}s: {e}"
61
91
  )
62
92
  else:
63
93
  _LOGGER.warning(
64
- f"{func.__name__} attempt {attempt}/{max_attempts} failed, "
94
+ f"{device_id}{func.__name__} attempt {attempt}/{max_attempts} failed, "
65
95
  f"retrying in {current_delay}s: {e}"
66
96
  )
67
97
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-jebao"
7
- version = "0.1.3"
7
+ version = "0.1.5"
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.3
3
+ Version: 0.1.5
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