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/retry.py ADDED
@@ -0,0 +1,77 @@
1
+ """Retry logic for handling transient failures."""
2
+ import asyncio
3
+ import functools
4
+ import logging
5
+ from typing import Callable, Type, Tuple
6
+
7
+ from .exceptions import JebaoConnectionError, JebaoTimeoutError
8
+
9
+ _LOGGER = logging.getLogger(__name__)
10
+
11
+
12
+ def async_retry(
13
+ max_attempts: int = 3,
14
+ delay: float = 0.5,
15
+ backoff: float = 2.0,
16
+ exceptions: Tuple[Type[Exception], ...] = (JebaoConnectionError, JebaoTimeoutError),
17
+ ):
18
+ """Decorator to retry async functions on specific exceptions.
19
+
20
+ Args:
21
+ max_attempts: Maximum number of attempts (default 3)
22
+ delay: Initial delay between retries in seconds (default 0.5)
23
+ backoff: Multiplier for delay on each retry (default 2.0)
24
+ exceptions: Tuple of exception types to retry on
25
+
26
+ Example:
27
+ @async_retry(max_attempts=3, delay=1.0)
28
+ async def send_command(self):
29
+ # Code that might fail due to garbage bytes
30
+ pass
31
+ """
32
+
33
+ def decorator(func: Callable):
34
+ @functools.wraps(func)
35
+ async def wrapper(*args, **kwargs):
36
+ attempt = 1
37
+ current_delay = delay
38
+
39
+ while attempt <= max_attempts:
40
+ try:
41
+ return await func(*args, **kwargs)
42
+ except exceptions as e:
43
+ if attempt == max_attempts:
44
+ _LOGGER.error(
45
+ f"{func.__name__} failed after {max_attempts} attempts: {e}"
46
+ )
47
+ raise
48
+
49
+ # Check if error message indicates garbage issue
50
+ error_msg = str(e).lower()
51
+ is_garbage_issue = (
52
+ "synchronization" in error_msg
53
+ or "garbage" in error_msg
54
+ or "invalid header" in error_msg
55
+ )
56
+
57
+ if is_garbage_issue:
58
+ _LOGGER.warning(
59
+ f"{func.__name__} attempt {attempt}/{max_attempts} failed "
60
+ f"due to garbage bytes, retrying in {current_delay}s: {e}"
61
+ )
62
+ else:
63
+ _LOGGER.warning(
64
+ f"{func.__name__} attempt {attempt}/{max_attempts} failed, "
65
+ f"retrying in {current_delay}s: {e}"
66
+ )
67
+
68
+ await asyncio.sleep(current_delay)
69
+ attempt += 1
70
+ current_delay *= backoff
71
+
72
+ # This should never be reached due to raise in loop
73
+ return None
74
+
75
+ return wrapper
76
+
77
+ return decorator
@@ -0,0 +1,367 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-jebao
3
+ Version: 0.1.0
4
+ Summary: Python library for controlling Jebao aquarium pumps
5
+ Home-page: https://github.com/jrigling/python-jebao
6
+ Author: Justin Rigling
7
+ Author-email: Justin Rigling <jrigling@gmail.com>
8
+ License-Expression: MIT
9
+ Project-URL: Homepage, https://github.com/jrigling/python-jebao
10
+ Project-URL: Repository, https://github.com/jrigling/python-jebao
11
+ Project-URL: Issues, https://github.com/jrigling/python-jebao/issues
12
+ Project-URL: Documentation, https://github.com/jrigling/python-jebao#readme
13
+ Keywords: jebao,aquarium,pump,home-assistant,iot
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Home Automation
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: netifaces>=0.11.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: black>=23.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.0; extra == "dev"
36
+ Provides-Extra: build
37
+ Requires-Dist: build>=0.10.0; extra == "build"
38
+ Requires-Dist: twine>=4.0.0; extra == "build"
39
+ Dynamic: author
40
+ Dynamic: home-page
41
+ Dynamic: license-file
42
+ Dynamic: requires-python
43
+
44
+ # python-jebao
45
+
46
+ Python library for controlling Jebao aquarium pumps over local network.
47
+
48
+ ## Supported Models
49
+
50
+ - **MDP-20000**: Variable-speed circulation pump (30-100% speed control)
51
+ - **MD-4.4**: 4-channel dosing pump (planned)
52
+
53
+ ## Features
54
+
55
+ - 🔍 **UDP Discovery** - Automatic device discovery with multi-subnet support
56
+ - 🔌 **Local Control** - Direct TCP control, no cloud dependency
57
+ - ⚡ **Full Protocol** - On/Off, speed control, feed mode, status monitoring
58
+ - 🏠 **Home Assistant Ready** - Designed for HA integration
59
+ - 🔒 **Type Safe** - Full type hints for better IDE support
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install python-jebao
65
+ ```
66
+
67
+ For development:
68
+ ```bash
69
+ git clone https://github.com/jrigling/python-jebao.git
70
+ cd python-jebao
71
+ pip install -e .
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ ### Discovery
77
+
78
+ ```python
79
+ import asyncio
80
+ from jebao import discover_devices
81
+
82
+ async def main():
83
+ # Discover all Jebao devices on network
84
+ devices = await discover_devices()
85
+
86
+ for device in devices:
87
+ print(f"Found: {device.model} at {device.ip_address}")
88
+ print(f" Device ID: {device.device_id}")
89
+ print(f" MAC: {device.mac_address}")
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ### Multi-Subnet Discovery
95
+
96
+ If you have multiple network interfaces (e.g., IoT VLAN, main network):
97
+
98
+ ```python
99
+ # Discover on all interfaces
100
+ devices = await discover_devices()
101
+
102
+ # Or specify interfaces explicitly
103
+ devices = await discover_devices(interfaces=['eth0', 'eth1'])
104
+ ```
105
+
106
+ ### Basic Control
107
+
108
+ ```python
109
+ from jebao import MDP20000Device
110
+
111
+ async def main():
112
+ # Connect to device
113
+ async with MDP20000Device(host="10.20.20.13") as pump:
114
+ # Ensure in manual mode (exits Program mode if needed)
115
+ await pump.ensure_manual_mode()
116
+
117
+ # Turn on
118
+ await pump.turn_on()
119
+
120
+ # Set speed to 75%
121
+ await pump.set_speed(75)
122
+
123
+ # Check status
124
+ await pump.update()
125
+ print(f"State: {pump.state.name}")
126
+ print(f"Speed: {pump.speed}%")
127
+ print(f"Is on: {pump.is_on}")
128
+
129
+ # Turn off
130
+ await pump.turn_off()
131
+
132
+ asyncio.run(main())
133
+ ```
134
+
135
+ ### Feed Mode
136
+
137
+ ```python
138
+ async def feed_mode_example():
139
+ async with MDP20000Device(host="10.20.20.13") as pump:
140
+ await pump.ensure_manual_mode()
141
+ await pump.turn_on()
142
+ await pump.set_speed(75)
143
+
144
+ # Start 2-minute feed (pump stops temporarily)
145
+ await pump.start_feed(minutes=2)
146
+ print("Feed started, pump will auto-resume in 2 minutes")
147
+
148
+ # Check status during feed
149
+ await pump.update()
150
+ print(f"In feed mode: {pump.is_feed_mode}")
151
+
152
+ # Or cancel early
153
+ # await pump.cancel_feed(resume_speed=75)
154
+ ```
155
+
156
+ ### Long-Running Connection
157
+
158
+ ```python
159
+ async def monitor_pump():
160
+ pump = MDP20000Device(host="10.20.20.13")
161
+
162
+ try:
163
+ await pump.connect()
164
+ await pump.ensure_manual_mode()
165
+
166
+ # Control pump
167
+ await pump.turn_on()
168
+ await pump.set_speed(50)
169
+
170
+ # Monitor status periodically
171
+ while True:
172
+ await pump.update()
173
+ print(f"Status: {pump.state.name} @ {pump.speed}%")
174
+ await asyncio.sleep(30)
175
+
176
+ finally:
177
+ await pump.disconnect()
178
+ ```
179
+
180
+ ## Device States
181
+
182
+ ```python
183
+ from jebao import DeviceState
184
+
185
+ # State values
186
+ DeviceState.OFF # 0x10 - Manual mode, stopped
187
+ DeviceState.ON # 0x11 - Manual mode, running
188
+ DeviceState.FEED # 0x15 - Feed mode (temporary pause)
189
+ DeviceState.PROGRAM # 0x19 - Program mode (scheduled)
190
+
191
+ # Check state
192
+ if pump.state == DeviceState.FEED:
193
+ print("Pump is in feed mode")
194
+
195
+ # Helper properties
196
+ pump.is_on # True if ON or FEED
197
+ pump.is_off # True if OFF
198
+ pump.is_feed_mode # True if FEED
199
+ pump.is_program_mode # True if PROGRAM
200
+ pump.is_manual_mode # True if ON or OFF
201
+ ```
202
+
203
+ ## API Reference
204
+
205
+ ### MDP20000Device
206
+
207
+ #### Connection
208
+ - `async connect(timeout=5.0)` - Connect and authenticate
209
+ - `async disconnect()` - Disconnect from device
210
+ - `is_connected` - Check connection status
211
+ - `async update()` - Refresh device status
212
+
213
+ #### Control
214
+ - `async turn_on()` - Turn pump on
215
+ - `async turn_off()` - Turn pump off
216
+ - `async set_speed(percentage)` - Set speed (30-100%)
217
+
218
+ #### Feed Mode
219
+ - `async set_feed_duration(minutes)` - Configure feed timer (1-10 minutes)
220
+ - `async start_feed(minutes=None)` - Start feed mode
221
+ - `async cancel_feed(resume_speed=None)` - Cancel feed and resume
222
+
223
+ #### Program Mode
224
+ - `async ensure_manual_mode()` - Exit Program mode if active
225
+
226
+ #### Properties
227
+ - `state: DeviceState` - Current state
228
+ - `speed: int` - Current speed (30-100)
229
+ - `is_on: bool` - Pump is running
230
+ - `is_off: bool` - Pump is stopped
231
+ - `is_feed_mode: bool` - In feed mode
232
+ - `is_program_mode: bool` - In program mode
233
+ - `is_manual_mode: bool` - In manual mode
234
+
235
+ ### Discovery
236
+
237
+ ```python
238
+ from jebao import JebaoDiscovery, discover_devices
239
+
240
+ # Convenience function
241
+ devices = await discover_devices(timeout=2.0, interfaces=['eth0'])
242
+
243
+ # Or use class directly
244
+ discovery = JebaoDiscovery()
245
+ devices = await discovery.discover(timeout=2.0, interfaces=['eth0'])
246
+
247
+ # Filter by model
248
+ mdp20000_devices = [d for d in devices if d.is_mdp20000]
249
+ md44_devices = [d for d in devices if d.is_md44]
250
+ ```
251
+
252
+ ### DiscoveredDevice
253
+
254
+ ```python
255
+ device.device_id # Device ID (e.g., "POAKSFXJNJ")
256
+ device.ip_address # IP address
257
+ device.mac_address # MAC address
258
+ device.product_key # Product key
259
+ device.model # Model name (e.g., "MDP-20000")
260
+ device.is_mdp20000 # True if MDP-20000
261
+ device.is_md44 # True if MD-4.4
262
+ ```
263
+
264
+ ## Home Assistant Integration
265
+
266
+ This library is designed for use with Home Assistant. See the separate
267
+ [homeassistant-jebao](https://github.com/jrigling/homeassistant-jebao) repository
268
+ for the ready-to-use HA integration.
269
+
270
+ ## Reliability Features
271
+
272
+ The library includes automatic retry logic for enhanced reliability:
273
+ - Commands automatically retry up to 3 times on transient failures
274
+ - Handles garbage byte accumulation from pump firmware
275
+ - Exponential backoff for retry delays
276
+ - Typical success rate >97% even with flaky network conditions
277
+
278
+ ## Multi-Subnet Considerations
279
+
280
+ When Home Assistant runs on a system with multiple network interfaces:
281
+
282
+ 1. **Automatic Discovery**: The library will scan all interfaces by default
283
+ 2. **Interface Selection**: You can specify which interfaces to scan
284
+ 3. **Broadcast Support**: Each interface gets its own broadcast address
285
+ 4. **IoT VLANs**: Works great with isolated IoT networks
286
+
287
+ Example: HA connected to both `192.168.1.0/24` (main) and `10.20.20.0/24` (IoT):
288
+
289
+ ```python
290
+ # Discovers on both networks automatically
291
+ devices = await discover_devices()
292
+
293
+ # Or be explicit
294
+ devices = await discover_devices(interfaces=['eth0', 'eth1'])
295
+ ```
296
+
297
+ ## Error Handling
298
+
299
+ ```python
300
+ from jebao import (
301
+ JebaoError,
302
+ JebaoConnectionError,
303
+ JebaoAuthenticationError,
304
+ JebaoCommandError,
305
+ JebaoTimeoutError,
306
+ JebaoInvalidStateError,
307
+ )
308
+
309
+ try:
310
+ async with MDP20000Device(host="10.20.20.13") as pump:
311
+ await pump.turn_on()
312
+ except JebaoConnectionError:
313
+ print("Failed to connect - check IP and network")
314
+ except JebaoAuthenticationError:
315
+ print("Authentication failed")
316
+ except JebaoCommandError:
317
+ print("Command execution failed")
318
+ except JebaoTimeoutError:
319
+ print("Operation timed out")
320
+ except JebaoError as e:
321
+ print(f"General error: {e}")
322
+ ```
323
+
324
+ ## Logging
325
+
326
+ The library uses Python's standard logging:
327
+
328
+ ```python
329
+ import logging
330
+
331
+ # Enable debug logging
332
+ logging.basicConfig(level=logging.DEBUG)
333
+
334
+ # Or just for jebao
335
+ logging.getLogger('jebao').setLevel(logging.DEBUG)
336
+ ```
337
+
338
+ ## Requirements
339
+
340
+ - Python 3.9+
341
+ - `netifaces` - For multi-interface network discovery
342
+
343
+ ## License
344
+
345
+ MIT License - See LICENSE file
346
+
347
+ ## Contributing
348
+
349
+ Contributions welcome! Please:
350
+
351
+ 1. Fork the repository
352
+ 2. Create a feature branch
353
+ 3. Add tests for new functionality
354
+ 4. Ensure all tests pass
355
+ 5. Submit a pull request
356
+
357
+ ## Credits
358
+
359
+ This library builds upon the excellent work from:
360
+ - [jebao-dosing-pump-md-4.4](https://github.com/tancou/jebao-dosing-pump-md-4.4) by [@tancou](https://github.com/tancou) - Original Node.js implementation for MD 4.4 pumps that provided the foundation for understanding the GizWits protocol
361
+
362
+ Protocol for MDP-20000 was reverse-engineered through packet capture analysis of the official Jebao mobile app.
363
+
364
+ ## Disclaimer
365
+
366
+ This is an unofficial library not affiliated with Jebao. Use at your own risk.
367
+ Device warranty may be affected by third-party control software.
@@ -0,0 +1,13 @@
1
+ jebao/__init__.py,sha256=0FRNVM0yhzfm1vJthqhe7TgFSfyRtHn_ao74LbkDXO4,761
2
+ jebao/const.py,sha256=KasF1RntYAwC_2Gv4wmF4VE4OQgTzQ5UAYugdvk8oBg,1621
3
+ jebao/device.py,sha256=1NF-EG6H5_DtavRdXdwdPFxHKBAU7zdrwBsPxMCfAL0,4023
4
+ jebao/discovery.py,sha256=nyXhu_BPXKzjF5lBeK2W62R_RtVhNnpcuulVvH20X9k,13365
5
+ jebao/exceptions.py,sha256=S8jO0kxaw9l_TMdITgZm2gx-olJdGB3746tLB8UahJ0,501
6
+ jebao/mdp20000.py,sha256=eyEDTpNm3DJ1uEIaqi2MLou1I63X9-GQzJXo-9xggok,9559
7
+ jebao/protocol.py,sha256=r2tZX_qnxJRY957qWXApicRDSXNW78HholkEsk4Opxc,25044
8
+ jebao/retry.py,sha256=N9biedjTigFRVmVrEAE_iPmuTW2GZJrv8B_zMQ8hC_A,2646
9
+ python_jebao-0.1.0.dist-info/licenses/LICENSE,sha256=xz5RpUswwNDP6Agh5an2XKA3BJfABxtPaeqPTDn1y2k,1071
10
+ python_jebao-0.1.0.dist-info/METADATA,sha256=btSVlVg--7HRXJrK2_RCA3clTTXYtbrkv0qWRejCEwI,10143
11
+ python_jebao-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ python_jebao-0.1.0.dist-info/top_level.txt,sha256=Bj9cYMUNXSj-Q_ViTIEIYJyfKOmnoywBAspe6ZprUkY,6
13
+ python_jebao-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justin Rigling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ jebao