python-jebao 0.1.0__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.
- jebao/__init__.py +35 -0
- jebao/const.py +68 -0
- jebao/device.py +139 -0
- jebao/discovery.py +399 -0
- jebao/exceptions.py +25 -0
- jebao/mdp20000.py +302 -0
- jebao/protocol.py +670 -0
- jebao/retry.py +77 -0
- python_jebao-0.1.0.dist-info/METADATA +367 -0
- python_jebao-0.1.0.dist-info/RECORD +13 -0
- python_jebao-0.1.0.dist-info/WHEEL +5 -0
- python_jebao-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_jebao-0.1.0.dist-info/top_level.txt +1 -0
jebao/mdp20000.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""MDP-20000 variable-speed circulation pump."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .const import (
|
|
6
|
+
COMMAND_TIMEOUT,
|
|
7
|
+
CommandOpcode,
|
|
8
|
+
DeviceState,
|
|
9
|
+
MAX_FEED_DURATION,
|
|
10
|
+
MAX_SPEED,
|
|
11
|
+
MIN_FEED_DURATION,
|
|
12
|
+
MIN_SPEED,
|
|
13
|
+
MODEL_MDP20000,
|
|
14
|
+
)
|
|
15
|
+
from .device import JebaoDevice
|
|
16
|
+
from .exceptions import JebaoCommandError, JebaoInvalidStateError
|
|
17
|
+
from .retry import async_retry
|
|
18
|
+
|
|
19
|
+
_LOGGER = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MDP20000Device(JebaoDevice):
|
|
23
|
+
"""MDP-20000 variable-speed circulation pump.
|
|
24
|
+
|
|
25
|
+
This class provides control for the Jebao MDP-20000 pump with features:
|
|
26
|
+
- On/Off control
|
|
27
|
+
- Variable speed (30-100%)
|
|
28
|
+
- Feed mode (temporary pause, 1-10 minutes)
|
|
29
|
+
- Program mode detection and exit
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, host: str, port: int = 12416, device_id: Optional[str] = None):
|
|
33
|
+
"""Initialize MDP-20000 device.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
host: Device IP address
|
|
37
|
+
port: TCP port (default 12416)
|
|
38
|
+
device_id: Device ID (from discovery)
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(host, port, device_id, MODEL_MDP20000)
|
|
41
|
+
self._feed_duration: int = 1 # Last configured feed duration
|
|
42
|
+
|
|
43
|
+
async def ensure_manual_mode(self, timeout: float = COMMAND_TIMEOUT) -> None:
|
|
44
|
+
"""Ensure device is in manual mode.
|
|
45
|
+
|
|
46
|
+
If device is in Program mode, exit to manual mode.
|
|
47
|
+
This should be called on connection before allowing user control.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
timeout: Command timeout
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
JebaoCommandError: Command failed
|
|
54
|
+
"""
|
|
55
|
+
await self.update()
|
|
56
|
+
|
|
57
|
+
if self.is_program_mode:
|
|
58
|
+
_LOGGER.warning("Device in Program mode, switching to Manual")
|
|
59
|
+
current_speed = self._speed or MAX_SPEED
|
|
60
|
+
|
|
61
|
+
await self._protocol.send_control_command(
|
|
62
|
+
CommandOpcode.EXIT_PROGRAM,
|
|
63
|
+
opcode2=0x00,
|
|
64
|
+
param1=current_speed,
|
|
65
|
+
param2=0x00,
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Verify state changed
|
|
70
|
+
await self.update()
|
|
71
|
+
if self.is_program_mode:
|
|
72
|
+
raise JebaoCommandError("Failed to exit Program mode")
|
|
73
|
+
|
|
74
|
+
_LOGGER.info("Successfully exited Program mode")
|
|
75
|
+
else:
|
|
76
|
+
_LOGGER.debug("Device already in manual mode")
|
|
77
|
+
|
|
78
|
+
@async_retry(max_attempts=3, delay=0.5)
|
|
79
|
+
async def turn_on(self, timeout: float = COMMAND_TIMEOUT) -> None:
|
|
80
|
+
"""Turn pump on.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
timeout: Command timeout
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
JebaoCommandError: Command failed
|
|
87
|
+
|
|
88
|
+
Note:
|
|
89
|
+
This method automatically retries up to 3 times on transient
|
|
90
|
+
failures (garbage bytes, connection issues).
|
|
91
|
+
"""
|
|
92
|
+
_LOGGER.info("Turning on pump")
|
|
93
|
+
|
|
94
|
+
await self._protocol.send_control_command(
|
|
95
|
+
CommandOpcode.TURN_ON_OFF,
|
|
96
|
+
opcode2=0x01, # 0x01 = ON
|
|
97
|
+
timeout=timeout,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Note: Call update() after a delay to see new state
|
|
101
|
+
# The pump needs time to process the command
|
|
102
|
+
|
|
103
|
+
@async_retry(max_attempts=3, delay=0.5)
|
|
104
|
+
async def turn_off(self, timeout: float = COMMAND_TIMEOUT) -> None:
|
|
105
|
+
"""Turn pump off.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
timeout: Command timeout
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
JebaoCommandError: Command failed
|
|
112
|
+
|
|
113
|
+
Note:
|
|
114
|
+
This method automatically retries up to 3 times on transient
|
|
115
|
+
failures (garbage bytes, connection issues).
|
|
116
|
+
"""
|
|
117
|
+
_LOGGER.info("Turning off pump")
|
|
118
|
+
|
|
119
|
+
await self._protocol.send_control_command(
|
|
120
|
+
CommandOpcode.TURN_ON_OFF,
|
|
121
|
+
opcode2=0x00, # 0x00 = OFF
|
|
122
|
+
timeout=timeout,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Note: Call update() after a delay to see new state
|
|
126
|
+
|
|
127
|
+
@async_retry(max_attempts=3, delay=0.5)
|
|
128
|
+
async def set_speed(
|
|
129
|
+
self, percentage: int, timeout: float = COMMAND_TIMEOUT
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Set pump speed.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
percentage: Speed percentage (30-100)
|
|
135
|
+
timeout: Command timeout
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: Invalid speed value
|
|
139
|
+
JebaoCommandError: Command failed
|
|
140
|
+
|
|
141
|
+
Note:
|
|
142
|
+
This method automatically retries up to 3 times on transient
|
|
143
|
+
failures (garbage bytes, connection issues).
|
|
144
|
+
"""
|
|
145
|
+
if not MIN_SPEED <= percentage <= MAX_SPEED:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Speed must be between {MIN_SPEED} and {MAX_SPEED}, got {percentage}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
_LOGGER.info("Setting speed to %d%%", percentage)
|
|
151
|
+
|
|
152
|
+
await self._protocol.send_control_command(
|
|
153
|
+
CommandOpcode.SET_SPEED,
|
|
154
|
+
opcode2=0x00,
|
|
155
|
+
param1=percentage,
|
|
156
|
+
param2=0x00,
|
|
157
|
+
timeout=timeout,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Note: Call update() after a delay to see new state
|
|
161
|
+
|
|
162
|
+
async def set_feed_duration(
|
|
163
|
+
self, minutes: int, timeout: float = COMMAND_TIMEOUT
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Set feed mode duration.
|
|
166
|
+
|
|
167
|
+
This configures the feed timer but does not start feed mode.
|
|
168
|
+
Use start_feed() to begin feed mode.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
minutes: Duration in minutes (1-10)
|
|
172
|
+
timeout: Command timeout
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: Invalid duration
|
|
176
|
+
JebaoCommandError: Command failed
|
|
177
|
+
"""
|
|
178
|
+
if not MIN_FEED_DURATION <= minutes <= MAX_FEED_DURATION:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Feed duration must be between {MIN_FEED_DURATION} and "
|
|
181
|
+
f"{MAX_FEED_DURATION} minutes, got {minutes}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
_LOGGER.info("Setting feed duration to %d minute(s)", minutes)
|
|
185
|
+
|
|
186
|
+
await self._protocol.send_control_command(
|
|
187
|
+
CommandOpcode.SET_FEED_DURATION,
|
|
188
|
+
opcode2=0x00,
|
|
189
|
+
param1=0x00,
|
|
190
|
+
param2=minutes,
|
|
191
|
+
timeout=timeout,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self._feed_duration = minutes
|
|
195
|
+
|
|
196
|
+
@async_retry(max_attempts=3, delay=1.0, backoff=1.5)
|
|
197
|
+
async def start_feed(
|
|
198
|
+
self, minutes: Optional[int] = None, timeout: float = COMMAND_TIMEOUT
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Start feed mode.
|
|
201
|
+
|
|
202
|
+
Feed mode temporarily stops the pump for the specified duration,
|
|
203
|
+
then automatically resumes at the previous speed.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
minutes: Duration in minutes (1-10). If None, uses last configured duration.
|
|
207
|
+
timeout: Command timeout
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ValueError: Invalid duration
|
|
211
|
+
JebaoCommandError: Command failed
|
|
212
|
+
JebaoInvalidStateError: Device not in valid state for feed mode
|
|
213
|
+
|
|
214
|
+
Note:
|
|
215
|
+
This method automatically retries up to 3 times on transient
|
|
216
|
+
failures (garbage bytes, connection issues) with longer delays
|
|
217
|
+
due to increased garbage accumulation risk.
|
|
218
|
+
"""
|
|
219
|
+
# Verify device is in manual mode
|
|
220
|
+
await self.update()
|
|
221
|
+
if not self.is_manual_mode:
|
|
222
|
+
raise JebaoInvalidStateError(
|
|
223
|
+
f"Cannot start feed from state {self._state.name}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if minutes is not None:
|
|
227
|
+
if not MIN_FEED_DURATION <= minutes <= MAX_FEED_DURATION:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Feed duration must be between {MIN_FEED_DURATION} and "
|
|
230
|
+
f"{MAX_FEED_DURATION} minutes, got {minutes}"
|
|
231
|
+
)
|
|
232
|
+
self._feed_duration = minutes
|
|
233
|
+
|
|
234
|
+
_LOGGER.info("Starting feed mode for %d minute(s)", self._feed_duration)
|
|
235
|
+
|
|
236
|
+
await self._protocol.send_control_command(
|
|
237
|
+
CommandOpcode.START_FEED,
|
|
238
|
+
opcode2=0x04,
|
|
239
|
+
param1=0x00,
|
|
240
|
+
param2=self._feed_duration,
|
|
241
|
+
timeout=timeout,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Update status
|
|
245
|
+
await self.update()
|
|
246
|
+
|
|
247
|
+
@async_retry(max_attempts=3, delay=1.0, backoff=1.5)
|
|
248
|
+
async def cancel_feed(
|
|
249
|
+
self, resume_speed: Optional[int] = None, timeout: float = COMMAND_TIMEOUT
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Cancel feed mode and resume pump.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
resume_speed: Speed to resume at (30-100). If None, uses current speed.
|
|
255
|
+
timeout: Command timeout
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
ValueError: Invalid speed
|
|
259
|
+
JebaoCommandError: Command failed
|
|
260
|
+
JebaoInvalidStateError: Device not in feed mode
|
|
261
|
+
|
|
262
|
+
Note:
|
|
263
|
+
This method automatically retries up to 3 times on transient
|
|
264
|
+
failures (garbage bytes, connection issues) with longer delays
|
|
265
|
+
due to increased garbage accumulation risk.
|
|
266
|
+
"""
|
|
267
|
+
# Verify device is in feed mode
|
|
268
|
+
await self.update()
|
|
269
|
+
if not self.is_feed_mode:
|
|
270
|
+
_LOGGER.warning("Device not in feed mode, ignoring cancel_feed")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
if resume_speed is None:
|
|
274
|
+
resume_speed = self._speed or MAX_SPEED
|
|
275
|
+
|
|
276
|
+
if not MIN_SPEED <= resume_speed <= MAX_SPEED:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"Resume speed must be between {MIN_SPEED} and {MAX_SPEED}, "
|
|
279
|
+
f"got {resume_speed}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
_LOGGER.info("Canceling feed mode, resuming at %d%%", resume_speed)
|
|
283
|
+
|
|
284
|
+
await self._protocol.send_control_command(
|
|
285
|
+
CommandOpcode.CANCEL_FEED,
|
|
286
|
+
opcode2=0x00,
|
|
287
|
+
param1=resume_speed,
|
|
288
|
+
param2=self._feed_duration,
|
|
289
|
+
timeout=timeout,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Update status
|
|
293
|
+
await self.update()
|
|
294
|
+
|
|
295
|
+
def __repr__(self) -> str:
|
|
296
|
+
"""String representation."""
|
|
297
|
+
return (
|
|
298
|
+
f"MDP20000Device(host={self.host!r}, "
|
|
299
|
+
f"device_id={self.device_id!r}, "
|
|
300
|
+
f"state={self._state.name if self._state else 'Unknown'}, "
|
|
301
|
+
f"speed={self._speed}%)"
|
|
302
|
+
)
|