python-selve-new 2.5.7__tar.gz → 2.5.9__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_selve_new-2.5.7 → python_selve_new-2.5.9}/PKG-INFO +1 -1
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/python_selve_new.egg-info/PKG-INFO +1 -1
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/python_selve_new.egg-info/SOURCES.txt +1 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/_version.py +3 -3
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/util/serial_transport.py +75 -13
- python_selve_new-2.5.9/tests/unit/test_serial_transport.py +143 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/FUNDING.yml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/architect.chatmode.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/ask.chatmode.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/code.chatmode.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/debug.chatmode.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/workflows/python-publish.yml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.github/workflows/tests.yml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.gitignore +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/.gitignore +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/misc.xml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/modules.xml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/python-selve-new.iml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/vcs.xml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/CHANGELOG.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/LICENSE +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/README.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/coverage_xdist_example.txt +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/debug_response.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/debug_test.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/direct_hardware_test.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/direct_hardware_test.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/generate_coverage.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/package.sh +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/pyproject.toml +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/python_selve_new.egg-info/dependency_links.txt +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/python_selve_new.egg-info/requires.txt +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/python_selve_new.egg-info/top_level.txt +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/release.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/run_all_tests.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/run_hardware_tests.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/run_integration_tests.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/run_mock_tests.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/run_single_test.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/run_tests.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/__init__.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/__init__.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/command.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/device.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/event.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/firmware.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/group.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/iveo.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/param.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/senSim.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/sender.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/sensor.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/commands/service.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/device.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/gateway.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/group.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/iveo.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/senSim.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/sender.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/sensor.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/util/__init__.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/util/errors.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/selve/util/protocol.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/setup.cfg +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/setup.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/setup_and_test.bat +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/__init__.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/conftest.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/README.md +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/__init__.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/conftest.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/test_device_integration.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/test_selve_gateway_integration.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/test_selve_hardware.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/test_selve_integration.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/test_import.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/test_replacement.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/__init__.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/mock_utils.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_command_coverage.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_commands.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_device.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_device_classes_coverage.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_device_commands_extended.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_gateway_configuration_issues.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_gateway_error_handling_fixed.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_group.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_group_commands.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_missing_components.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_mock_commands.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_mock_devices_and_groups.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_mock_sensors_and_senders.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_param_commands_extended.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_port_discovery.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_advanced_coverage.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_core_coverage.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_edge_cases.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_gateway.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_init_comprehensive.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_init_response_coverage.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_init_simple.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_main_class_extensive.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_sender_commands_extended.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_sensim_commands_extended.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_sensor_commands_extended.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_service_command_errors.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_service_commands.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_util.py +0 -0
- {python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_utility_coverage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-selve-new
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.9
|
|
4
4
|
Summary: Python library for interfacing with selve devices using the USB-RF controller. Written completely new.
|
|
5
5
|
Home-page: https://github.com/Kannix2005/python-selve-new
|
|
6
6
|
Author: Stefan Altheimer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-selve-new
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.9
|
|
4
4
|
Summary: Python library for interfacing with selve devices using the USB-RF controller. Written completely new.
|
|
5
5
|
Home-page: https://github.com/Kannix2005/python-selve-new
|
|
6
6
|
Author: Stefan Altheimer
|
|
@@ -101,6 +101,7 @@ tests/unit/test_selve_main_class_extensive.py
|
|
|
101
101
|
tests/unit/test_sender_commands_extended.py
|
|
102
102
|
tests/unit/test_sensim_commands_extended.py
|
|
103
103
|
tests/unit/test_sensor_commands_extended.py
|
|
104
|
+
tests/unit/test_serial_transport.py
|
|
104
105
|
tests/unit/test_service_command_errors.py
|
|
105
106
|
tests/unit/test_service_commands.py
|
|
106
107
|
tests/unit/test_util.py
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 5,
|
|
21
|
+
__version__ = version = '2.5.9'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 5, 9)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gc04157d25'
|
|
@@ -5,6 +5,38 @@ from typing import Optional
|
|
|
5
5
|
import serialx
|
|
6
6
|
from serialx import Parity
|
|
7
7
|
|
|
8
|
+
# Any data chunk arriving within this window extends the deadline.
|
|
9
|
+
# Only a truly dead/disconnected port triggers a reconnect.
|
|
10
|
+
_IDLE_TIMEOUT = 60.0
|
|
11
|
+
|
|
12
|
+
# The gateway protocol uses XML-RPC framing. Every message ends with one of
|
|
13
|
+
# these two tags; there are no other root-level closing tags in the protocol.
|
|
14
|
+
_END_TAGS = (b"</methodResponse>", b"</methodCall>")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_messages(buffer: bytes) -> tuple:
|
|
18
|
+
"""Extract all complete XML messages from *buffer*.
|
|
19
|
+
|
|
20
|
+
Returns (messages, remaining_bytes) where *messages* is a list of decoded
|
|
21
|
+
strings and *remaining_bytes* is the unconsumed tail of the buffer.
|
|
22
|
+
"""
|
|
23
|
+
messages: list = []
|
|
24
|
+
while True:
|
|
25
|
+
earliest_end = None
|
|
26
|
+
for tag in _END_TAGS:
|
|
27
|
+
pos = buffer.find(tag)
|
|
28
|
+
if pos >= 0:
|
|
29
|
+
end = pos + len(tag)
|
|
30
|
+
if earliest_end is None or end < earliest_end:
|
|
31
|
+
earliest_end = end
|
|
32
|
+
if earliest_end is None:
|
|
33
|
+
break
|
|
34
|
+
msg = buffer[:earliest_end].decode(errors="ignore").strip()
|
|
35
|
+
buffer = buffer[earliest_end:]
|
|
36
|
+
if msg:
|
|
37
|
+
messages.append(msg)
|
|
38
|
+
return messages, buffer
|
|
39
|
+
|
|
8
40
|
|
|
9
41
|
class SerialTransport:
|
|
10
42
|
"""Async serial transport using serialx (replaces pyserial + background thread)."""
|
|
@@ -36,6 +68,7 @@ class SerialTransport:
|
|
|
36
68
|
|
|
37
69
|
async def ensure_open(self) -> None:
|
|
38
70
|
if not self.is_open:
|
|
71
|
+
self._logger.info("Serial: opening %s", self._port)
|
|
39
72
|
self._reader, self._writer = await serialx.open_serial_connection(
|
|
40
73
|
url=self._port,
|
|
41
74
|
baudrate=self._baudrate,
|
|
@@ -46,8 +79,10 @@ class SerialTransport:
|
|
|
46
79
|
rtscts=False,
|
|
47
80
|
dsrdtr=False,
|
|
48
81
|
)
|
|
82
|
+
self._logger.info("Serial: %s opened", self._port)
|
|
49
83
|
|
|
50
84
|
async def close(self) -> None:
|
|
85
|
+
self._logger.info("Serial: closing %s", self._port)
|
|
51
86
|
try:
|
|
52
87
|
if self._writer and not self._writer.is_closing():
|
|
53
88
|
self._writer.close()
|
|
@@ -56,34 +91,63 @@ class SerialTransport:
|
|
|
56
91
|
self._logger.debug("Serial close failed", exc_info=True)
|
|
57
92
|
self._reader = None
|
|
58
93
|
self._writer = None
|
|
94
|
+
self._logger.info("Serial: %s closed", self._port)
|
|
59
95
|
|
|
60
96
|
async def start_reader(self, rx_queue: asyncio.Queue) -> None:
|
|
61
97
|
"""Opens the serial port and starts the async reader task."""
|
|
62
98
|
await self.ensure_open()
|
|
63
99
|
self._rx_queue = rx_queue
|
|
64
100
|
if self._reader_task and not self._reader_task.done():
|
|
101
|
+
self._logger.info("Serial: reader task already running")
|
|
65
102
|
return
|
|
103
|
+
self._logger.info("Serial: starting reader task")
|
|
66
104
|
self._reader_task = asyncio.create_task(
|
|
67
105
|
self._reader_loop(), name="selve-serial-reader"
|
|
68
106
|
)
|
|
69
107
|
|
|
70
108
|
async def stop_reader(self) -> None:
|
|
71
109
|
if self._reader_task:
|
|
110
|
+
self._logger.info("Serial: stopping reader task")
|
|
72
111
|
self._reader_task.cancel()
|
|
73
112
|
try:
|
|
74
113
|
await self._reader_task
|
|
75
114
|
except asyncio.CancelledError:
|
|
76
115
|
pass
|
|
77
116
|
self._reader_task = None
|
|
117
|
+
self._logger.info("Serial: reader task stopped")
|
|
78
118
|
|
|
79
119
|
async def _reader_loop(self) -> None:
|
|
80
|
-
|
|
120
|
+
"""Read raw bytes and dispatch complete XML messages by framing on closing tags.
|
|
121
|
+
|
|
122
|
+
Using read() instead of readline() avoids depending on \\n as a message
|
|
123
|
+
delimiter. The Selve protocol is XML-RPC: every gateway message ends with
|
|
124
|
+
either </methodResponse> or </methodCall>. We scan the byte buffer for
|
|
125
|
+
these tags and dispatch as soon as a complete message is available.
|
|
126
|
+
|
|
127
|
+
A 60-second idle timeout (no bytes at all) triggers a reconnect. This is
|
|
128
|
+
far more conservative than the old 12-second readline hack and only fires
|
|
129
|
+
when the serial port is truly dead.
|
|
130
|
+
"""
|
|
131
|
+
self._logger.info("Serial: reader loop started")
|
|
132
|
+
buffer = b""
|
|
81
133
|
while True:
|
|
82
134
|
try:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
135
|
+
await self.ensure_open()
|
|
136
|
+
assert self._reader is not None
|
|
137
|
+
chunk = await asyncio.wait_for(
|
|
138
|
+
self._reader.read(4096), timeout=_IDLE_TIMEOUT
|
|
139
|
+
)
|
|
140
|
+
except asyncio.TimeoutError:
|
|
141
|
+
self._logger.warning(
|
|
142
|
+
"Serial: no data for %.0fs — reconnecting", _IDLE_TIMEOUT
|
|
143
|
+
)
|
|
144
|
+
buffer = b""
|
|
145
|
+
await self.close()
|
|
146
|
+
await asyncio.sleep(1.0)
|
|
147
|
+
await self.ensure_open()
|
|
148
|
+
continue
|
|
86
149
|
except asyncio.CancelledError:
|
|
150
|
+
self._logger.info("Serial: reader loop cancelled")
|
|
87
151
|
break
|
|
88
152
|
except (OSError, EOFError) as exc:
|
|
89
153
|
self._logger.error("Serial read error: %s", exc)
|
|
@@ -94,19 +158,17 @@ class SerialTransport:
|
|
|
94
158
|
await asyncio.sleep(1)
|
|
95
159
|
continue
|
|
96
160
|
|
|
97
|
-
if not
|
|
161
|
+
if not chunk:
|
|
98
162
|
await asyncio.sleep(0.01)
|
|
99
163
|
continue
|
|
100
164
|
|
|
101
|
-
|
|
102
|
-
if decoded == "":
|
|
103
|
-
if buffer and self._rx_queue:
|
|
104
|
-
self._logger.debug("Serial RX: %s", buffer)
|
|
105
|
-
await self._rx_queue.put(buffer)
|
|
106
|
-
buffer = ""
|
|
107
|
-
continue
|
|
165
|
+
buffer += chunk
|
|
108
166
|
|
|
109
|
-
buffer
|
|
167
|
+
new_messages, buffer = _extract_messages(buffer)
|
|
168
|
+
for msg in new_messages:
|
|
169
|
+
self._logger.debug("Serial RX: %s", msg)
|
|
170
|
+
if self._rx_queue:
|
|
171
|
+
await self._rx_queue.put(msg)
|
|
110
172
|
|
|
111
173
|
async def write(self, payload: bytes) -> None:
|
|
112
174
|
await self.ensure_open()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from selve.util.serial_transport import _extract_messages
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Pure synchronous tests for _extract_messages
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
def test_single_method_response():
|
|
13
|
+
xml = b"<methodResponse><array><string>selve.GW.service.ping</string></array></methodResponse>"
|
|
14
|
+
msgs, remaining = _extract_messages(xml)
|
|
15
|
+
assert len(msgs) == 1
|
|
16
|
+
assert "</methodResponse>" in msgs[0]
|
|
17
|
+
assert "selve.GW.service.ping" in msgs[0]
|
|
18
|
+
assert remaining == b""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_single_method_call():
|
|
22
|
+
xml = b"<methodCall><methodName>selve.GW.event.device</methodName><array></array></methodCall>"
|
|
23
|
+
msgs, remaining = _extract_messages(xml)
|
|
24
|
+
assert len(msgs) == 1
|
|
25
|
+
assert "</methodCall>" in msgs[0]
|
|
26
|
+
assert "event.device" in msgs[0]
|
|
27
|
+
assert remaining == b""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_two_messages_in_one_chunk():
|
|
31
|
+
chunk = (
|
|
32
|
+
b"<methodResponse><array><string>selve.GW.service.ping</string></array></methodResponse>"
|
|
33
|
+
b"<methodCall><methodName>selve.GW.event.dutyCycle</methodName><array></array></methodCall>"
|
|
34
|
+
)
|
|
35
|
+
msgs, remaining = _extract_messages(chunk)
|
|
36
|
+
assert len(msgs) == 2
|
|
37
|
+
assert any("methodResponse" in m for m in msgs)
|
|
38
|
+
assert any("methodCall" in m for m in msgs)
|
|
39
|
+
assert remaining == b""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_incomplete_message_stays_in_buffer():
|
|
43
|
+
partial = b"<methodResponse><array><string>selve.GW.service.ping</string></array>"
|
|
44
|
+
msgs, remaining = _extract_messages(partial)
|
|
45
|
+
assert msgs == []
|
|
46
|
+
assert remaining == partial
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_message_split_across_chunks():
|
|
50
|
+
xml = b"<methodResponse><array><string>selve.GW.service.ping</string></array></methodResponse>"
|
|
51
|
+
mid = len(xml) // 2
|
|
52
|
+
# First chunk: no complete message yet
|
|
53
|
+
msgs1, buf = _extract_messages(xml[:mid])
|
|
54
|
+
assert msgs1 == []
|
|
55
|
+
# Second chunk completes the message
|
|
56
|
+
msgs2, remaining = _extract_messages(buf + xml[mid:])
|
|
57
|
+
assert len(msgs2) == 1
|
|
58
|
+
assert "</methodResponse>" in msgs2[0]
|
|
59
|
+
assert remaining == b""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_xml_preamble_included_in_message():
|
|
63
|
+
xml = (
|
|
64
|
+
b'<?xml version="1.0"? encoding="UTF-8">'
|
|
65
|
+
b"<methodResponse><array><string>selve.GW.service.ping</string></array></methodResponse>"
|
|
66
|
+
)
|
|
67
|
+
msgs, remaining = _extract_messages(xml)
|
|
68
|
+
assert len(msgs) == 1
|
|
69
|
+
assert "methodResponse" in msgs[0]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_garbage_before_message_is_included():
|
|
73
|
+
"""Leading whitespace/garbage bytes before the opening tag stay in the message."""
|
|
74
|
+
xml = b"\r\n <methodResponse><array></array></methodResponse>"
|
|
75
|
+
msgs, remaining = _extract_messages(xml)
|
|
76
|
+
assert len(msgs) == 1
|
|
77
|
+
assert "methodResponse" in msgs[0]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_empty_buffer():
|
|
81
|
+
msgs, remaining = _extract_messages(b"")
|
|
82
|
+
assert msgs == []
|
|
83
|
+
assert remaining == b""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_fault_response_uses_method_response_tag():
|
|
87
|
+
"""Fault responses are wrapped in <methodResponse>, same closing tag."""
|
|
88
|
+
xml = b"<methodResponse><fault><array><string>Error</string><int>-1</int></array></fault></methodResponse>"
|
|
89
|
+
msgs, remaining = _extract_messages(xml)
|
|
90
|
+
assert len(msgs) == 1
|
|
91
|
+
assert "fault" in msgs[0]
|
|
92
|
+
assert remaining == b""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Async test: idle timeout triggers reconnect
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_idle_timeout_triggers_reconnect():
|
|
101
|
+
"""If no data arrives for _IDLE_TIMEOUT seconds, close+ensure_open are called."""
|
|
102
|
+
import selve.util.serial_transport as st
|
|
103
|
+
from selve.util.serial_transport import SerialTransport
|
|
104
|
+
|
|
105
|
+
transport = SerialTransport(port="COM_TEST", logger=MagicMock())
|
|
106
|
+
|
|
107
|
+
# Reader that never produces data (simulates dead port)
|
|
108
|
+
reader = asyncio.StreamReader()
|
|
109
|
+
transport._reader = reader
|
|
110
|
+
transport._writer = MagicMock(is_closing=MagicMock(return_value=False))
|
|
111
|
+
transport._rx_queue = asyncio.Queue()
|
|
112
|
+
|
|
113
|
+
close_calls = []
|
|
114
|
+
ensure_calls = []
|
|
115
|
+
|
|
116
|
+
async def fake_close():
|
|
117
|
+
close_calls.append(1)
|
|
118
|
+
transport._reader = None
|
|
119
|
+
transport._writer = None
|
|
120
|
+
|
|
121
|
+
async def fake_ensure():
|
|
122
|
+
ensure_calls.append(1)
|
|
123
|
+
transport._reader = asyncio.StreamReader()
|
|
124
|
+
transport._writer = MagicMock(is_closing=MagicMock(return_value=False))
|
|
125
|
+
|
|
126
|
+
transport.close = fake_close
|
|
127
|
+
transport.ensure_open = fake_ensure
|
|
128
|
+
|
|
129
|
+
original = st._IDLE_TIMEOUT
|
|
130
|
+
st._IDLE_TIMEOUT = 0.05
|
|
131
|
+
try:
|
|
132
|
+
task = asyncio.create_task(transport._reader_loop())
|
|
133
|
+
await asyncio.sleep(0.25)
|
|
134
|
+
task.cancel()
|
|
135
|
+
try:
|
|
136
|
+
await task
|
|
137
|
+
except asyncio.CancelledError:
|
|
138
|
+
pass
|
|
139
|
+
finally:
|
|
140
|
+
st._IDLE_TIMEOUT = original
|
|
141
|
+
|
|
142
|
+
assert len(close_calls) >= 1, "close() should be called on idle timeout"
|
|
143
|
+
assert len(ensure_calls) >= 1, "ensure_open() should be called after reconnect"
|
|
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
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/.idea/inspectionProfiles/profiles_settings.xml
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/python_selve_new.egg-info/dependency_links.txt
RENAMED
|
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
|
|
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
|
|
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
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/test_device_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/integration/test_selve_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_device_classes_coverage.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_device_commands_extended.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_gateway_configuration_issues.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_gateway_error_handling_fixed.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_mock_devices_and_groups.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_mock_sensors_and_senders.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_param_commands_extended.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_advanced_coverage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_init_comprehensive.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_init_response_coverage.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_selve_main_class_extensive.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_sender_commands_extended.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_sensim_commands_extended.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.7 → python_selve_new-2.5.9}/tests/unit/test_sensor_commands_extended.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|