python-selve-new 2.5.11__tar.gz → 2.5.12__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.11 → python_selve_new-2.5.12}/CHANGELOG.md +13 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/PKG-INFO +1 -1
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/python_selve_new.egg-info/PKG-INFO +1 -1
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/python_selve_new.egg-info/SOURCES.txt +1 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/__init__.py +78 -41
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/_version.py +3 -3
- python_selve_new-2.5.12/tests/unit/test_perf_improvements.py +194 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_advanced_coverage.py +1 -1
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_core_coverage.py +1 -1
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_edge_cases.py +1 -1
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_init_comprehensive.py +2 -2
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_init_simple.py +1 -1
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/FUNDING.yml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/architect.chatmode.md +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/ask.chatmode.md +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/code.chatmode.md +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/debug.chatmode.md +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/workflows/python-publish.yml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.github/workflows/tests.yml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.gitignore +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.idea/.gitignore +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.idea/misc.xml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.idea/modules.xml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.idea/python-selve-new.iml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/.idea/vcs.xml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/LICENSE +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/README.md +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/coverage_xdist_example.txt +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/debug_response.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/debug_test.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/direct_hardware_test.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/direct_hardware_test.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/generate_coverage.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/package.sh +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/pyproject.toml +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/python_selve_new.egg-info/dependency_links.txt +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/python_selve_new.egg-info/requires.txt +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/python_selve_new.egg-info/top_level.txt +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/release.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/run_all_tests.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/run_hardware_tests.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/run_integration_tests.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/run_mock_tests.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/run_single_test.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/run_tests.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/__init__.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/command.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/device.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/event.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/firmware.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/group.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/iveo.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/param.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/senSim.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/sender.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/sensor.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/commands/service.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/device.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/gateway.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/group.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/iveo.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/senSim.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/sender.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/sensor.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/util/__init__.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/util/errors.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/util/protocol.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/selve/util/serial_transport.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/setup.cfg +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/setup.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/setup_and_test.bat +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/__init__.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/conftest.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/README.md +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/__init__.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/conftest.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/test_device_integration.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/test_selve_gateway_integration.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/test_selve_hardware.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/test_selve_integration.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/test_import.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/test_replacement.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/__init__.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/mock_utils.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_command_coverage.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_commands.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_device.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_device_classes_coverage.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_device_commands_extended.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_gateway_configuration_issues.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_gateway_error_handling_fixed.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_group.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_group_commands.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_missing_components.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_mock_commands.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_mock_devices_and_groups.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_mock_sensors_and_senders.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_param_commands_extended.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_port_discovery.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_gateway.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_init_response_coverage.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_main_class_extensive.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_sender_commands_extended.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_sensim_commands_extended.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_sensor_commands_extended.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_serial_transport.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_service_command_errors.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_service_commands.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_util.py +0 -0
- {python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_utility_coverage.py +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.5.12] - 2026-06-12
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Response-paced command transmission**: the TX loop now waits for the previous command's response (with a 5s safety timeout) instead of sleeping a fixed 100ms after every serial write. Command round-trips drop from ~130ms to gateway speed (~30-40ms); full device discovery is roughly 3x faster. Verified against live gateway hardware.
|
|
9
|
+
- **Device-aware update callbacks**: `register_callback` callbacks may now accept the changed device as a single positional argument, so consumers can update only the affected entity. Parameterless callbacks keep working unchanged. Callbacks are now fired only when a device actually changed (from `addOrUpdateDevice`), no longer after every gateway response, and a failing callback no longer breaks the dispatch of the remaining ones.
|
|
10
|
+
- Hot-path logging now uses lazy `%s` formatting; the malformed-XML-header workaround only runs when the broken header is actually present.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Gateway error responses stalled callers for 10s**: a fault reply from the gateway never resolved the pending command future, so `executeCommandSyncWithResponse` always ran into its full 10s timeout (and another 10s on retry). Error responses now resolve the waiting future with `False` immediately.
|
|
14
|
+
- `updateAllDevices()` crashed with `AttributeError`: it iterated dict keys (ints) instead of device objects.
|
|
15
|
+
- `processTeachResponse` was called without `await`, so teach/scan result processing (including the event callback and event queue delivery) never actually ran.
|
|
16
|
+
- Sender events were stored into the sensor device registry (`SelveTypes.SENSOR` instead of `SelveTypes.SENDER`), overwriting sensors that shared the same id.
|
|
17
|
+
|
|
5
18
|
## [2.5.0] - 2026-02-11
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-selve-new
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.12
|
|
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.12
|
|
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
|
|
@@ -89,6 +89,7 @@ tests/unit/test_mock_commands.py
|
|
|
89
89
|
tests/unit/test_mock_devices_and_groups.py
|
|
90
90
|
tests/unit/test_mock_sensors_and_senders.py
|
|
91
91
|
tests/unit/test_param_commands_extended.py
|
|
92
|
+
tests/unit/test_perf_improvements.py
|
|
92
93
|
tests/unit/test_port_discovery.py
|
|
93
94
|
tests/unit/test_selve_advanced_coverage.py
|
|
94
95
|
tests/unit/test_selve_core_coverage.py
|
|
@@ -12,6 +12,7 @@ except ImportError:
|
|
|
12
12
|
__version__ = "unknown"
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
+
import inspect
|
|
15
16
|
import logging
|
|
16
17
|
import time
|
|
17
18
|
from collections import deque
|
|
@@ -60,7 +61,8 @@ class Selve:
|
|
|
60
61
|
|
|
61
62
|
def __init__(self, port=None, discover=True, develop=False, logger=None, loop=None):
|
|
62
63
|
# Gateway state
|
|
63
|
-
|
|
64
|
+
# callback -> whether it accepts the changed device as argument
|
|
65
|
+
self._callbacks: dict = {}
|
|
64
66
|
self._eventCallbacks = set()
|
|
65
67
|
self.lastLogEvent = None
|
|
66
68
|
self.state = None
|
|
@@ -301,6 +303,17 @@ class Selve:
|
|
|
301
303
|
self._pending_futures.append(future)
|
|
302
304
|
|
|
303
305
|
await self._sendCommandToGateway(command)
|
|
306
|
+
|
|
307
|
+
if future is not None:
|
|
308
|
+
# Pace transmissions by the response instead of a fixed
|
|
309
|
+
# delay: the gateway answers in order, so the next command
|
|
310
|
+
# may go out as soon as this one's response has arrived.
|
|
311
|
+
# A timeout keeps a lost response from stalling the queue.
|
|
312
|
+
await asyncio.wait({future}, timeout=5)
|
|
313
|
+
else:
|
|
314
|
+
# Fire-and-forget commands still produce a gateway reply;
|
|
315
|
+
# keep a small gap so the gateway is not flooded.
|
|
316
|
+
await asyncio.sleep(0.05)
|
|
304
317
|
except asyncio.CancelledError:
|
|
305
318
|
break
|
|
306
319
|
except Exception as e:
|
|
@@ -325,17 +338,13 @@ class Selve:
|
|
|
325
338
|
try:
|
|
326
339
|
resp = await self.processResponse(msg)
|
|
327
340
|
if isinstance(resp, ErrorResponse):
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if not fut.done():
|
|
336
|
-
fut.set_result(resp)
|
|
337
|
-
break
|
|
338
|
-
else:
|
|
341
|
+
# A fault is the gateway's reply to a command: resolve the
|
|
342
|
+
# waiting future with False right away instead of letting
|
|
343
|
+
# the caller run into its 10s timeout.
|
|
344
|
+
if not self._resolve_next_future(False):
|
|
345
|
+
self._LOGGER.debug("(Selve RX): error response without pending future -> %s", resp)
|
|
346
|
+
elif resp not in (False, True, None):
|
|
347
|
+
if not self._resolve_next_future(resp):
|
|
339
348
|
self._LOGGER.debug("(Selve RX): response without pending future -> %s", resp)
|
|
340
349
|
self.rxQ.task_done()
|
|
341
350
|
except Exception as e:
|
|
@@ -346,6 +355,17 @@ class Selve:
|
|
|
346
355
|
pass
|
|
347
356
|
|
|
348
357
|
|
|
358
|
+
def _resolve_next_future(self, result):
|
|
359
|
+
"""Resolve the oldest pending command future with the given result."""
|
|
360
|
+
while self._pending_futures:
|
|
361
|
+
fut = self._pending_futures.popleft()
|
|
362
|
+
if fut.cancelled():
|
|
363
|
+
continue
|
|
364
|
+
if not fut.done():
|
|
365
|
+
fut.set_result(result)
|
|
366
|
+
return True
|
|
367
|
+
return False
|
|
368
|
+
|
|
349
369
|
async def stopWorker(self):
|
|
350
370
|
self._LOGGER.debug("Stopping worker")
|
|
351
371
|
self._pauseWorker.set()
|
|
@@ -386,13 +406,37 @@ class Selve:
|
|
|
386
406
|
return True
|
|
387
407
|
|
|
388
408
|
|
|
389
|
-
def register_callback(self, callback: Callable
|
|
390
|
-
"""Register callback, called when
|
|
391
|
-
self._callbacks.add(callback)
|
|
409
|
+
def register_callback(self, callback: Callable) -> None:
|
|
410
|
+
"""Register callback, called when a device changes state.
|
|
392
411
|
|
|
393
|
-
|
|
412
|
+
The callback may optionally accept the changed device as its single
|
|
413
|
+
positional argument; parameterless callbacks keep working and are
|
|
414
|
+
invoked without arguments.
|
|
415
|
+
"""
|
|
416
|
+
accepts_device = True
|
|
417
|
+
try:
|
|
418
|
+
inspect.signature(callback).bind(None)
|
|
419
|
+
except TypeError:
|
|
420
|
+
accepts_device = False
|
|
421
|
+
except ValueError:
|
|
422
|
+
# Builtins without introspectable signature: assume no args
|
|
423
|
+
accepts_device = False
|
|
424
|
+
self._callbacks[callback] = accepts_device
|
|
425
|
+
|
|
426
|
+
def remove_callback(self, callback: Callable) -> None:
|
|
394
427
|
"""Remove previously registered callback."""
|
|
395
|
-
self._callbacks.
|
|
428
|
+
self._callbacks.pop(callback, None)
|
|
429
|
+
|
|
430
|
+
def _fire_callbacks(self, device=None) -> None:
|
|
431
|
+
"""Notify registered callbacks, passing the changed device if known."""
|
|
432
|
+
for callback, accepts_device in list(self._callbacks.items()):
|
|
433
|
+
try:
|
|
434
|
+
if accepts_device:
|
|
435
|
+
callback(device)
|
|
436
|
+
else:
|
|
437
|
+
callback()
|
|
438
|
+
except Exception:
|
|
439
|
+
self._LOGGER.exception("Error in update callback")
|
|
396
440
|
|
|
397
441
|
def register_event_callback(self, callback: Callable[[], None]) -> None:
|
|
398
442
|
"""Register callback, called when other events take place."""
|
|
@@ -418,7 +462,7 @@ class Selve:
|
|
|
418
462
|
|
|
419
463
|
async def _sendCommandToGateway(self, command: Command):
|
|
420
464
|
commandstr = command.serializeToXML()
|
|
421
|
-
self._LOGGER.debug('Gateway writing: '
|
|
465
|
+
self._LOGGER.debug('Gateway writing: %s', commandstr)
|
|
422
466
|
try:
|
|
423
467
|
if self._transport is None:
|
|
424
468
|
if self._port is None:
|
|
@@ -426,11 +470,9 @@ class Selve:
|
|
|
426
470
|
await self._build_transport(self._port)
|
|
427
471
|
|
|
428
472
|
await self._transport.write(commandstr)
|
|
429
|
-
# small pause to give the gateway time to answer
|
|
430
|
-
await asyncio.sleep(0.1)
|
|
431
473
|
|
|
432
474
|
except (OSError, IOError) as se:
|
|
433
|
-
self._LOGGER.info('Serial error, trying to reconnect once... '
|
|
475
|
+
self._LOGGER.info('Serial error, trying to reconnect once... %s', se)
|
|
434
476
|
await self.recover()
|
|
435
477
|
|
|
436
478
|
try:
|
|
@@ -438,13 +480,12 @@ class Selve:
|
|
|
438
480
|
if self._transport is None and self._port is not None:
|
|
439
481
|
await self._build_transport(self._port)
|
|
440
482
|
await self._transport.write(commandstr)
|
|
441
|
-
|
|
442
|
-
|
|
483
|
+
|
|
443
484
|
except Exception as e:
|
|
444
|
-
self._LOGGER.error("error communicating:
|
|
485
|
+
self._LOGGER.error("error communicating: %s ; Please restart the integration!", e)
|
|
445
486
|
|
|
446
487
|
except Exception as e:
|
|
447
|
-
self._LOGGER.error("error communicating:
|
|
488
|
+
self._LOGGER.error("error communicating: %s ; Please restart the integration!", e)
|
|
448
489
|
|
|
449
490
|
async def processResponse(self, xmlstr):
|
|
450
491
|
"""Processes an XML String into a response object. Returns False if something went wrong or the gateway returned an error."""
|
|
@@ -453,11 +494,13 @@ class Selve:
|
|
|
453
494
|
# return the ready to eat response
|
|
454
495
|
|
|
455
496
|
# The selve device sometimes answers a badformed header. This is a patch
|
|
456
|
-
xmlstr = str(xmlstr)
|
|
497
|
+
xmlstr = str(xmlstr)
|
|
498
|
+
if '<?xml version="1.0"? encoding="UTF-8">' in xmlstr:
|
|
499
|
+
xmlstr = xmlstr.replace('<?xml version="1.0"? encoding="UTF-8">', '<?xml version="1.0" encoding="UTF-8"?>')
|
|
457
500
|
try:
|
|
458
501
|
res = untangle.parse(xmlstr)
|
|
459
502
|
except Exception as e:
|
|
460
|
-
self._LOGGER.error("Error in XML:
|
|
503
|
+
self._LOGGER.error("Error in XML: %s : %s", e, xmlstr)
|
|
461
504
|
return False
|
|
462
505
|
try:
|
|
463
506
|
if not hasattr(res, 'methodResponse') and not hasattr(res, 'methodCall'):
|
|
@@ -492,16 +535,14 @@ class Selve:
|
|
|
492
535
|
if isinstance(response, SenderTeachResultResponse) \
|
|
493
536
|
or isinstance(response, SensorTeachResultResponse)\
|
|
494
537
|
or isinstance(response, DeviceScanResultResponse):
|
|
495
|
-
self.processTeachResponse(response)
|
|
538
|
+
await self.processTeachResponse(response)
|
|
496
539
|
return True
|
|
497
540
|
|
|
498
|
-
for callback in self._callbacks:
|
|
499
|
-
callback()
|
|
500
541
|
return response
|
|
501
542
|
|
|
502
543
|
|
|
503
544
|
except Exception as e:
|
|
504
|
-
self._LOGGER.error("Error in response processing:
|
|
545
|
+
self._LOGGER.error("Error in response processing: %s : %s", e, xmlstr)
|
|
505
546
|
return False
|
|
506
547
|
|
|
507
548
|
def create_error(self, obj):
|
|
@@ -901,13 +942,13 @@ class Selve:
|
|
|
901
942
|
|
|
902
943
|
|
|
903
944
|
async def updateAllDevices(self):
|
|
904
|
-
for device in self.devices[SelveTypes.DEVICE.value]:
|
|
945
|
+
for device in list(self.devices[SelveTypes.DEVICE.value].values()):
|
|
905
946
|
await self.updateCommeoDeviceValues(device.id)
|
|
906
|
-
for sensor in self.devices[SelveTypes.SENSOR.value]:
|
|
947
|
+
for sensor in list(self.devices[SelveTypes.SENSOR.value].values()):
|
|
907
948
|
await self.updateSensorValuesAsync(sensor.id)
|
|
908
|
-
for senSim in self.devices[SelveTypes.SENSIM.value]:
|
|
949
|
+
for senSim in list(self.devices[SelveTypes.SENSIM.value].values()):
|
|
909
950
|
await self.updateSenSimValuesAsync(senSim.id)
|
|
910
|
-
for sender in self.devices[SelveTypes.SENDER.value]:
|
|
951
|
+
for sender in list(self.devices[SelveTypes.SENDER.value].values()):
|
|
911
952
|
await self.updateSenderValuesAsync(sender.id)
|
|
912
953
|
|
|
913
954
|
|
|
@@ -917,8 +958,7 @@ class Selve:
|
|
|
917
958
|
# add in gateway
|
|
918
959
|
|
|
919
960
|
# if there is a callback for updates, call it
|
|
920
|
-
|
|
921
|
-
callback()
|
|
961
|
+
self._fire_callbacks(device)
|
|
922
962
|
|
|
923
963
|
def getDevice(self, id: int, type: SelveTypes) -> SelveDevice | SelveSensor | SelveSender | SelveGroup | SelveSenSim | None:
|
|
924
964
|
if id in self.devices[type.value]:
|
|
@@ -1057,7 +1097,7 @@ class Selve:
|
|
|
1057
1097
|
|
|
1058
1098
|
sender.lastEvent = response.event
|
|
1059
1099
|
sender.name = response.senderName
|
|
1060
|
-
self.addOrUpdateDevice(sender, SelveTypes.
|
|
1100
|
+
self.addOrUpdateDevice(sender, SelveTypes.SENDER)
|
|
1061
1101
|
|
|
1062
1102
|
if isinstance(response, LogEventResponse):
|
|
1063
1103
|
self.lastLogEvent = response
|
|
@@ -1118,9 +1158,6 @@ class Selve:
|
|
|
1118
1158
|
dev.unreachable = True
|
|
1119
1159
|
self.addOrUpdateDevice(dev, SelveTypes.DEVICE)
|
|
1120
1160
|
|
|
1121
|
-
for callback in self._callbacks:
|
|
1122
|
-
callback()
|
|
1123
|
-
|
|
1124
1161
|
|
|
1125
1162
|
### Service
|
|
1126
1163
|
|
|
@@ -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.12'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 5, 12)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g62288d04e'
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the performance improvements:
|
|
3
|
+
- response-paced TX loop (no fixed 100ms delay)
|
|
4
|
+
- ErrorResponse resolves the pending future immediately
|
|
5
|
+
- callbacks receive the changed device (with zero-arg compatibility)
|
|
6
|
+
- updateAllDevices iterates device objects, not ids
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
from unittest.mock import Mock, AsyncMock
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from selve import Selve
|
|
15
|
+
from selve.commands.service import ServicePing
|
|
16
|
+
from selve.util.errors import ErrorResponse
|
|
17
|
+
from selve.util.protocol import SelveTypes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_selve():
|
|
21
|
+
return Selve(port=None, discover=False, develop=False, logger=Mock())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestResolveNextFuture:
|
|
25
|
+
def test_resolves_oldest_pending_future(self):
|
|
26
|
+
selve = _make_selve()
|
|
27
|
+
loop = asyncio.new_event_loop()
|
|
28
|
+
try:
|
|
29
|
+
fut1 = loop.create_future()
|
|
30
|
+
fut2 = loop.create_future()
|
|
31
|
+
selve._pending_futures.append(fut1)
|
|
32
|
+
selve._pending_futures.append(fut2)
|
|
33
|
+
|
|
34
|
+
assert selve._resolve_next_future("result") is True
|
|
35
|
+
assert fut1.result() == "result"
|
|
36
|
+
assert not fut2.done()
|
|
37
|
+
finally:
|
|
38
|
+
loop.close()
|
|
39
|
+
|
|
40
|
+
def test_skips_cancelled_futures(self):
|
|
41
|
+
selve = _make_selve()
|
|
42
|
+
loop = asyncio.new_event_loop()
|
|
43
|
+
try:
|
|
44
|
+
fut1 = loop.create_future()
|
|
45
|
+
fut1.cancel()
|
|
46
|
+
fut2 = loop.create_future()
|
|
47
|
+
selve._pending_futures.append(fut1)
|
|
48
|
+
selve._pending_futures.append(fut2)
|
|
49
|
+
|
|
50
|
+
assert selve._resolve_next_future(False) is True
|
|
51
|
+
assert fut2.result() is False
|
|
52
|
+
finally:
|
|
53
|
+
loop.close()
|
|
54
|
+
|
|
55
|
+
def test_returns_false_without_pending_future(self):
|
|
56
|
+
selve = _make_selve()
|
|
57
|
+
assert selve._resolve_next_future("x") is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_error_response_resolves_future_immediately():
|
|
62
|
+
"""A gateway fault must resolve the waiting command future with False
|
|
63
|
+
instead of letting the caller run into its 10s timeout."""
|
|
64
|
+
selve = _make_selve()
|
|
65
|
+
selve.rxQ = asyncio.Queue()
|
|
66
|
+
|
|
67
|
+
async def fake_process(msg):
|
|
68
|
+
return ErrorResponse("Method not supported", 2)
|
|
69
|
+
|
|
70
|
+
selve.processResponse = fake_process
|
|
71
|
+
|
|
72
|
+
future = asyncio.get_running_loop().create_future()
|
|
73
|
+
selve._pending_futures.append(future)
|
|
74
|
+
await selve.rxQ.put("<dummy/>")
|
|
75
|
+
|
|
76
|
+
task = asyncio.create_task(selve._dispatch_loop())
|
|
77
|
+
try:
|
|
78
|
+
for _ in range(100):
|
|
79
|
+
if future.done():
|
|
80
|
+
break
|
|
81
|
+
await asyncio.sleep(0.01)
|
|
82
|
+
assert future.done(), "future was not resolved by the error response"
|
|
83
|
+
assert future.result() is False
|
|
84
|
+
finally:
|
|
85
|
+
selve._stopThread.set()
|
|
86
|
+
task.cancel()
|
|
87
|
+
try:
|
|
88
|
+
await task
|
|
89
|
+
except asyncio.CancelledError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_tx_loop_waits_for_response_before_next_command():
|
|
95
|
+
"""The TX loop must not send the next command before the previous
|
|
96
|
+
command's response future is resolved."""
|
|
97
|
+
selve = _make_selve()
|
|
98
|
+
selve.txQ = asyncio.Queue()
|
|
99
|
+
send_times = []
|
|
100
|
+
|
|
101
|
+
async def fake_send(command):
|
|
102
|
+
send_times.append(time.monotonic())
|
|
103
|
+
|
|
104
|
+
selve._sendCommandToGateway = fake_send
|
|
105
|
+
|
|
106
|
+
loop = asyncio.get_running_loop()
|
|
107
|
+
fut1 = loop.create_future()
|
|
108
|
+
fut2 = loop.create_future()
|
|
109
|
+
await selve.txQ.put((ServicePing(), fut1))
|
|
110
|
+
await selve.txQ.put((ServicePing(), fut2))
|
|
111
|
+
|
|
112
|
+
task = asyncio.create_task(selve._tx_loop())
|
|
113
|
+
try:
|
|
114
|
+
await asyncio.sleep(0.15)
|
|
115
|
+
# only the first command may have been sent so far
|
|
116
|
+
assert len(send_times) == 1
|
|
117
|
+
fut1.set_result("pong")
|
|
118
|
+
await asyncio.sleep(0.1)
|
|
119
|
+
assert len(send_times) == 2
|
|
120
|
+
finally:
|
|
121
|
+
selve._stopThread.set()
|
|
122
|
+
task.cancel()
|
|
123
|
+
try:
|
|
124
|
+
await task
|
|
125
|
+
except asyncio.CancelledError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestDeviceCallbacks:
|
|
130
|
+
def test_callback_receives_device(self):
|
|
131
|
+
selve = _make_selve()
|
|
132
|
+
received = []
|
|
133
|
+
selve.register_callback(lambda device: received.append(device))
|
|
134
|
+
|
|
135
|
+
sentinel = object()
|
|
136
|
+
selve._fire_callbacks(sentinel)
|
|
137
|
+
assert received == [sentinel]
|
|
138
|
+
|
|
139
|
+
def test_zero_arg_callback_still_works(self):
|
|
140
|
+
selve = _make_selve()
|
|
141
|
+
calls = []
|
|
142
|
+
|
|
143
|
+
def legacy_callback():
|
|
144
|
+
calls.append(1)
|
|
145
|
+
|
|
146
|
+
selve.register_callback(legacy_callback)
|
|
147
|
+
selve._fire_callbacks(object())
|
|
148
|
+
assert calls == [1]
|
|
149
|
+
|
|
150
|
+
def test_failing_callback_does_not_break_others(self):
|
|
151
|
+
selve = _make_selve()
|
|
152
|
+
received = []
|
|
153
|
+
|
|
154
|
+
def bad_callback(device):
|
|
155
|
+
raise RuntimeError("boom")
|
|
156
|
+
|
|
157
|
+
selve.register_callback(bad_callback)
|
|
158
|
+
selve.register_callback(lambda device: received.append(device))
|
|
159
|
+
selve._fire_callbacks("dev")
|
|
160
|
+
assert received == ["dev"]
|
|
161
|
+
|
|
162
|
+
def test_remove_callback(self):
|
|
163
|
+
selve = _make_selve()
|
|
164
|
+
cb = lambda device: None
|
|
165
|
+
selve.register_callback(cb)
|
|
166
|
+
selve.remove_callback(cb)
|
|
167
|
+
assert cb not in selve._callbacks
|
|
168
|
+
|
|
169
|
+
def test_add_or_update_device_fires_callback_with_device(self):
|
|
170
|
+
selve = _make_selve()
|
|
171
|
+
received = []
|
|
172
|
+
selve.register_callback(lambda device: received.append(device))
|
|
173
|
+
|
|
174
|
+
dev = Mock()
|
|
175
|
+
dev.id = 7
|
|
176
|
+
selve.addOrUpdateDevice(dev, SelveTypes.DEVICE)
|
|
177
|
+
assert received == [dev]
|
|
178
|
+
assert selve.devices[SelveTypes.DEVICE.value][7] is dev
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_update_all_devices_iterates_device_objects():
|
|
183
|
+
selve = _make_selve()
|
|
184
|
+
dev = Mock()
|
|
185
|
+
dev.id = 3
|
|
186
|
+
selve.devices[SelveTypes.DEVICE.value][3] = dev
|
|
187
|
+
|
|
188
|
+
selve.updateCommeoDeviceValues = AsyncMock()
|
|
189
|
+
selve.updateSensorValuesAsync = AsyncMock()
|
|
190
|
+
selve.updateSenSimValuesAsync = AsyncMock()
|
|
191
|
+
selve.updateSenderValuesAsync = AsyncMock()
|
|
192
|
+
|
|
193
|
+
await selve.updateAllDevices()
|
|
194
|
+
selve.updateCommeoDeviceValues.assert_awaited_once_with(3)
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_advanced_coverage.py
RENAMED
|
@@ -58,7 +58,7 @@ class TestSelveAdvancedCoverage(unittest.TestCase):
|
|
|
58
58
|
gateway = Selve(logger=self.mock_logger)
|
|
59
59
|
|
|
60
60
|
# Test that error callbacks are properly initialized
|
|
61
|
-
self.assertIsInstance(gateway._callbacks,
|
|
61
|
+
self.assertIsInstance(gateway._callbacks, dict)
|
|
62
62
|
self.assertIsInstance(gateway._eventCallbacks, set)
|
|
63
63
|
|
|
64
64
|
# Test device container structure
|
|
@@ -99,7 +99,7 @@ class TestSelveCoreClasses(unittest.TestCase):
|
|
|
99
99
|
self.assertEqual(len(gateway._callbacks), 0)
|
|
100
100
|
self.assertEqual(len(gateway._eventCallbacks), 0)
|
|
101
101
|
|
|
102
|
-
self.assertIsInstance(gateway._callbacks,
|
|
102
|
+
self.assertIsInstance(gateway._callbacks, dict)
|
|
103
103
|
self.assertIsInstance(gateway._eventCallbacks, set)
|
|
104
104
|
|
|
105
105
|
def test_state_properties(self):
|
|
@@ -176,7 +176,7 @@ class TestSelveEdgeCases:
|
|
|
176
176
|
# Check data structures - txQ might be None initially
|
|
177
177
|
assert selve.txQ is None or isinstance(selve.txQ, list)
|
|
178
178
|
assert isinstance(selve.devices, dict)
|
|
179
|
-
assert isinstance(selve._callbacks,
|
|
179
|
+
assert isinstance(selve._callbacks, dict)
|
|
180
180
|
assert isinstance(selve._eventCallbacks, set)
|
|
181
181
|
|
|
182
182
|
def test_serial_attribute_handling(self):
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_init_comprehensive.py
RENAMED
|
@@ -47,7 +47,7 @@ class TestSelveInit:
|
|
|
47
47
|
assert selve._port == "COM5"
|
|
48
48
|
assert selve._LOGGER == logger
|
|
49
49
|
assert selve.loop == loop
|
|
50
|
-
assert isinstance(selve._callbacks,
|
|
50
|
+
assert isinstance(selve._callbacks, dict)
|
|
51
51
|
assert isinstance(selve._eventCallbacks, set)
|
|
52
52
|
assert selve.utilization == 0
|
|
53
53
|
assert selve.sendingBlocked == DutyMode.NOT_BLOCKED
|
|
@@ -63,7 +63,7 @@ class TestSelveInit:
|
|
|
63
63
|
import logging
|
|
64
64
|
assert isinstance(selve._LOGGER, logging.Logger)
|
|
65
65
|
assert selve.loop is None
|
|
66
|
-
assert isinstance(selve._callbacks,
|
|
66
|
+
assert isinstance(selve._callbacks, dict)
|
|
67
67
|
assert isinstance(selve._eventCallbacks, set)
|
|
68
68
|
|
|
69
69
|
def test_init_devices_structure(self):
|
|
@@ -18,7 +18,7 @@ class TestSelveInitSimple:
|
|
|
18
18
|
"""Test basic Selve initialization"""
|
|
19
19
|
selve = Selve(port=None, discover=False, develop=False, logger=Mock())
|
|
20
20
|
assert selve._port is None
|
|
21
|
-
assert selve._callbacks ==
|
|
21
|
+
assert selve._callbacks == {}
|
|
22
22
|
assert selve._eventCallbacks == set()
|
|
23
23
|
assert selve.utilization == 0
|
|
24
24
|
assert len(selve.devices) == 6
|
|
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.11 → python_selve_new-2.5.12}/.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
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/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.11 → python_selve_new-2.5.12}/tests/integration/test_device_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/integration/test_selve_hardware.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/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.11 → python_selve_new-2.5.12}/tests/unit/test_device_classes_coverage.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_device_commands_extended.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_gateway_configuration_issues.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/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.11 → python_selve_new-2.5.12}/tests/unit/test_mock_devices_and_groups.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_mock_sensors_and_senders.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_param_commands_extended.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_init_response_coverage.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_selve_main_class_extensive.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_sender_commands_extended.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_sensim_commands_extended.py
RENAMED
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_sensor_commands_extended.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_selve_new-2.5.11 → python_selve_new-2.5.12}/tests/unit/test_service_command_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|