python-jebao 0.1.2__tar.gz → 0.1.3__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.
- {python_jebao-0.1.2 → python_jebao-0.1.3}/PKG-INFO +1 -1
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/__init__.py +1 -1
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/protocol.py +76 -65
- {python_jebao-0.1.2 → python_jebao-0.1.3}/pyproject.toml +1 -1
- {python_jebao-0.1.2 → python_jebao-0.1.3}/python_jebao.egg-info/PKG-INFO +1 -1
- {python_jebao-0.1.2 → python_jebao-0.1.3}/LICENSE +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/README.md +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/const.py +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/device.py +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/discovery.py +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/exceptions.py +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/mdp20000.py +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/jebao/retry.py +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/python_jebao.egg-info/SOURCES.txt +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/python_jebao.egg-info/dependency_links.txt +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/python_jebao.egg-info/requires.txt +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/python_jebao.egg-info/top_level.txt +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/setup.cfg +0 -0
- {python_jebao-0.1.2 → python_jebao-0.1.3}/setup.py +0 -0
|
@@ -55,7 +55,10 @@ class JebaoProtocol:
|
|
|
55
55
|
self._sequence = 1
|
|
56
56
|
self._keepalive_task: Optional[asyncio.Task] = None
|
|
57
57
|
self._passcode: Optional[bytes] = None
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
# Locks for thread-safe operation
|
|
60
|
+
self._request_lock = asyncio.Lock() # Serialize entire request-response cycles
|
|
61
|
+
self._read_lock = asyncio.Lock() # Low-level read serialization
|
|
59
62
|
|
|
60
63
|
# Message dispatcher
|
|
61
64
|
self._message_queues: Dict[int, asyncio.Queue] = {}
|
|
@@ -360,39 +363,41 @@ class JebaoProtocol:
|
|
|
360
363
|
JebaoConnectionError: Not connected
|
|
361
364
|
JebaoTimeoutError: Request timed out
|
|
362
365
|
"""
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
response
|
|
381
|
-
|
|
382
|
-
|
|
366
|
+
# Serialize requests - only one operation at a time
|
|
367
|
+
async with self._request_lock:
|
|
368
|
+
if not self.is_connected:
|
|
369
|
+
raise JebaoConnectionError("Not connected")
|
|
370
|
+
|
|
371
|
+
# Drain any unexpected leftover bytes as safety net (very short timeout)
|
|
372
|
+
# Normally padding is read explicitly, but this catches edge cases
|
|
373
|
+
await self._drain_garbage_bytes(timeout=0.01)
|
|
374
|
+
|
|
375
|
+
# Send simple status request (0x90)
|
|
376
|
+
# Format: 00 00 00 03 04 00 00 90 02
|
|
377
|
+
# The last byte (02) requests values, and response will be message type 91 or 0x00
|
|
378
|
+
request = bytes.fromhex("0000000304000090 02")
|
|
379
|
+
_LOGGER.debug(f"Sending status request: {request.hex()}")
|
|
380
|
+
await self._send_raw(request)
|
|
381
|
+
_LOGGER.debug("Status request sent, waiting for response...")
|
|
382
|
+
|
|
383
|
+
# Read response directly (no dispatcher)
|
|
384
|
+
try:
|
|
385
|
+
response = await asyncio.wait_for(self._read_raw(), timeout=timeout)
|
|
386
|
+
except asyncio.TimeoutError as err:
|
|
387
|
+
raise JebaoTimeoutError("Status request timed out") from err
|
|
383
388
|
|
|
384
|
-
|
|
389
|
+
_LOGGER.debug(f"Status response length: {len(response)}, data: {response.hex()[:100]}...")
|
|
385
390
|
|
|
386
|
-
|
|
391
|
+
# Note: Don't drain here - pump may send unsolicited updates that we shouldn't consume
|
|
387
392
|
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
if len(response) < 10:
|
|
394
|
+
raise JebaoCommandError(f"Invalid status response: too short ({len(response)} bytes)")
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
# Log the message type and first few bytes for debugging
|
|
397
|
+
msg_type = response[7] if len(response) > 7 else 0
|
|
398
|
+
_LOGGER.debug(f"Status response type: 0x{msg_type:02x}, bytes 10-12: {response[10:12].hex() if len(response) > 11 else 'N/A'}")
|
|
394
399
|
|
|
395
|
-
|
|
400
|
+
return response
|
|
396
401
|
|
|
397
402
|
async def send_control_command(
|
|
398
403
|
self,
|
|
@@ -416,46 +421,48 @@ class JebaoProtocol:
|
|
|
416
421
|
JebaoCommandError: Command failed
|
|
417
422
|
JebaoTimeoutError: Command timed out
|
|
418
423
|
"""
|
|
419
|
-
|
|
420
|
-
|
|
424
|
+
# Serialize requests - only one operation at a time
|
|
425
|
+
async with self._request_lock:
|
|
426
|
+
if not self.is_connected:
|
|
427
|
+
raise JebaoConnectionError("Not connected")
|
|
421
428
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
command = self._build_control_command(opcode1, opcode2, param1, param2)
|
|
427
|
-
_LOGGER.debug(
|
|
428
|
-
"Sending control command: opcode1=0x%02x, opcode2=0x%02x, param1=%d, param2=%d",
|
|
429
|
-
opcode1,
|
|
430
|
-
opcode2,
|
|
431
|
-
param1,
|
|
432
|
-
param2,
|
|
433
|
-
)
|
|
434
|
-
await self._send_raw(command)
|
|
435
|
-
|
|
436
|
-
# Read ACK (message type 0x94) directly
|
|
437
|
-
# The pump sends an ACK for control commands
|
|
438
|
-
try:
|
|
439
|
-
ack = await asyncio.wait_for(self._read_raw(), timeout=timeout)
|
|
440
|
-
ack_type = ack[7] if len(ack) > 7 else 0
|
|
441
|
-
_LOGGER.debug("Control command ACK received: type=0x%02x, %d bytes", ack_type, len(ack))
|
|
429
|
+
# Drain any unexpected leftover bytes as safety net (very short timeout)
|
|
430
|
+
# Normally padding is read explicitly, but this catches edge cases
|
|
431
|
+
await self._drain_garbage_bytes(timeout=0.01)
|
|
442
432
|
|
|
443
|
-
|
|
444
|
-
|
|
433
|
+
command = self._build_control_command(opcode1, opcode2, param1, param2)
|
|
434
|
+
_LOGGER.debug(
|
|
435
|
+
"Sending control command: opcode1=0x%02x, opcode2=0x%02x, param1=%d, param2=%d",
|
|
436
|
+
opcode1,
|
|
437
|
+
opcode2,
|
|
438
|
+
param1,
|
|
439
|
+
param2,
|
|
440
|
+
)
|
|
441
|
+
await self._send_raw(command)
|
|
442
|
+
|
|
443
|
+
# Read ACK (message type 0x94) directly
|
|
444
|
+
# The pump sends an ACK for control commands
|
|
445
445
|
try:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
_LOGGER.
|
|
449
|
-
"Pump sent unsolicited status update after control command: type=0x%02x",
|
|
450
|
-
status_type,
|
|
451
|
-
)
|
|
452
|
-
except asyncio.TimeoutError:
|
|
453
|
-
# No status update sent, that's OK
|
|
454
|
-
_LOGGER.debug("No unsolicited status update after control command")
|
|
446
|
+
ack = await asyncio.wait_for(self._read_raw(), timeout=timeout)
|
|
447
|
+
ack_type = ack[7] if len(ack) > 7 else 0
|
|
448
|
+
_LOGGER.debug("Control command ACK received: type=0x%02x, %d bytes", ack_type, len(ack))
|
|
455
449
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
450
|
+
# After ACK, pump may send unsolicited status update
|
|
451
|
+
# Try to read it with short timeout
|
|
452
|
+
try:
|
|
453
|
+
status_update = await asyncio.wait_for(self._read_raw(), timeout=0.5)
|
|
454
|
+
status_type = status_update[7] if len(status_update) > 7 else 0
|
|
455
|
+
_LOGGER.info(
|
|
456
|
+
"Pump sent unsolicited status update after control command: type=0x%02x",
|
|
457
|
+
status_type,
|
|
458
|
+
)
|
|
459
|
+
except asyncio.TimeoutError:
|
|
460
|
+
# No status update sent, that's OK
|
|
461
|
+
_LOGGER.debug("No unsolicited status update after control command")
|
|
462
|
+
|
|
463
|
+
except asyncio.TimeoutError:
|
|
464
|
+
_LOGGER.warning("Control command sent but no ACK received (pump may still process it)")
|
|
465
|
+
# Don't raise - pump may still process the command even without ACK
|
|
459
466
|
|
|
460
467
|
def _build_control_command(
|
|
461
468
|
self, opcode1: int, opcode2: int, param1: int, param2: int
|
|
@@ -582,6 +589,8 @@ class JebaoProtocol:
|
|
|
582
589
|
self._writer.write(data)
|
|
583
590
|
await self._writer.drain()
|
|
584
591
|
except Exception as err:
|
|
592
|
+
_LOGGER.error("Send failed, closing connection: %s", err)
|
|
593
|
+
await self.disconnect()
|
|
585
594
|
raise JebaoConnectionError(f"Send failed: {err}") from err
|
|
586
595
|
|
|
587
596
|
async def _read_raw(self) -> bytes:
|
|
@@ -684,6 +693,8 @@ class JebaoProtocol:
|
|
|
684
693
|
return result
|
|
685
694
|
except asyncio.IncompleteReadError as err:
|
|
686
695
|
_LOGGER.error("IncompleteReadError - connection closed by peer")
|
|
696
|
+
# Mark connection as closed so reconnection can happen
|
|
697
|
+
await self.disconnect()
|
|
687
698
|
raise JebaoConnectionError("Connection closed") from err
|
|
688
699
|
except JebaoConnectionError:
|
|
689
700
|
raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|