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/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
+ )