python-selve-new 2.5.2__tar.gz → 2.5.4__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.
Files changed (109) hide show
  1. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/PKG-INFO +1 -1
  2. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/pyproject.toml +4 -1
  3. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/python_selve_new.egg-info/PKG-INFO +1 -1
  4. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/__init__.py +94 -26
  5. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/_version.py +3 -3
  6. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/test_selve_gateway_integration.py +1 -1
  7. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_edge_cases.py +3 -2
  8. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_init_comprehensive.py +2 -1
  9. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/FUNDING.yml +0 -0
  10. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/architect.chatmode.md +0 -0
  11. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/ask.chatmode.md +0 -0
  12. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/code.chatmode.md +0 -0
  13. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/debug.chatmode.md +0 -0
  14. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/workflows/python-publish.yml +0 -0
  15. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.github/workflows/tests.yml +0 -0
  16. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.gitignore +0 -0
  17. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.idea/.gitignore +0 -0
  18. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  19. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.idea/misc.xml +0 -0
  20. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.idea/modules.xml +0 -0
  21. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.idea/python-selve-new.iml +0 -0
  22. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/.idea/vcs.xml +0 -0
  23. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/CHANGELOG.md +0 -0
  24. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/LICENSE +0 -0
  25. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/README.md +0 -0
  26. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/coverage_xdist_example.txt +0 -0
  27. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/debug_response.py +0 -0
  28. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/debug_test.bat +0 -0
  29. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/direct_hardware_test.bat +0 -0
  30. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/direct_hardware_test.py +0 -0
  31. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/generate_coverage.bat +0 -0
  32. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/package.sh +0 -0
  33. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/python_selve_new.egg-info/SOURCES.txt +0 -0
  34. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/python_selve_new.egg-info/dependency_links.txt +0 -0
  35. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/python_selve_new.egg-info/requires.txt +0 -0
  36. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/python_selve_new.egg-info/top_level.txt +0 -0
  37. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/release.py +0 -0
  38. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/run_all_tests.bat +0 -0
  39. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/run_hardware_tests.bat +0 -0
  40. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/run_integration_tests.bat +0 -0
  41. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/run_mock_tests.bat +0 -0
  42. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/run_single_test.bat +0 -0
  43. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/run_tests.bat +0 -0
  44. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/__init__.py +0 -0
  45. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/command.py +0 -0
  46. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/device.py +0 -0
  47. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/event.py +0 -0
  48. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/firmware.py +0 -0
  49. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/group.py +0 -0
  50. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/iveo.py +0 -0
  51. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/param.py +0 -0
  52. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/senSim.py +0 -0
  53. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/sender.py +0 -0
  54. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/sensor.py +0 -0
  55. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/commands/service.py +0 -0
  56. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/device.py +0 -0
  57. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/gateway.py +0 -0
  58. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/group.py +0 -0
  59. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/iveo.py +0 -0
  60. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/senSim.py +0 -0
  61. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/sender.py +0 -0
  62. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/sensor.py +0 -0
  63. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/util/__init__.py +0 -0
  64. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/util/errors.py +0 -0
  65. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/util/protocol.py +0 -0
  66. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/selve/util/serial_transport.py +0 -0
  67. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/setup.cfg +0 -0
  68. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/setup.py +0 -0
  69. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/setup_and_test.bat +0 -0
  70. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/__init__.py +0 -0
  71. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/conftest.py +0 -0
  72. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/README.md +0 -0
  73. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/__init__.py +0 -0
  74. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/conftest.py +0 -0
  75. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/test_device_integration.py +0 -0
  76. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/test_selve_hardware.py +0 -0
  77. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/integration/test_selve_integration.py +0 -0
  78. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/test_import.py +0 -0
  79. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/test_replacement.py +0 -0
  80. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/__init__.py +0 -0
  81. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/mock_utils.py +0 -0
  82. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_command_coverage.py +0 -0
  83. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_commands.py +0 -0
  84. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_device.py +0 -0
  85. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_device_classes_coverage.py +0 -0
  86. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_device_commands_extended.py +0 -0
  87. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_gateway_configuration_issues.py +0 -0
  88. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_gateway_error_handling_fixed.py +0 -0
  89. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_group.py +0 -0
  90. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_group_commands.py +0 -0
  91. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_missing_components.py +0 -0
  92. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_mock_commands.py +0 -0
  93. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_mock_devices_and_groups.py +0 -0
  94. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_mock_sensors_and_senders.py +0 -0
  95. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_param_commands_extended.py +0 -0
  96. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_port_discovery.py +0 -0
  97. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_advanced_coverage.py +0 -0
  98. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_core_coverage.py +0 -0
  99. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_gateway.py +0 -0
  100. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_init_response_coverage.py +0 -0
  101. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_init_simple.py +0 -0
  102. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_selve_main_class_extensive.py +0 -0
  103. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_sender_commands_extended.py +0 -0
  104. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_sensim_commands_extended.py +0 -0
  105. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_sensor_commands_extended.py +0 -0
  106. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_service_command_errors.py +0 -0
  107. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_service_commands.py +0 -0
  108. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/tests/unit/test_util.py +0 -0
  109. {python_selve_new-2.5.2 → python_selve_new-2.5.4}/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.2
3
+ Version: 2.5.4
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
@@ -59,4 +59,7 @@ markers = [
59
59
  "unit: marks tests as unit tests",
60
60
  ]
61
61
  asyncio_mode = "auto"
62
- asyncio_default_fixture_loop_scope = "function"
62
+ asyncio_default_fixture_loop_scope = "function"
63
+ filterwarnings = [
64
+ "ignore:coroutine 'Selve._movement_poll_loop' was never awaited:RuntimeWarning",
65
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-selve-new
3
- Version: 2.5.2
3
+ Version: 2.5.4
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
@@ -12,6 +12,7 @@ except ImportError:
12
12
  __version__ = "unknown"
13
13
 
14
14
  import asyncio
15
+ import logging
15
16
  import time
16
17
  from collections import deque
17
18
  from itertools import chain
@@ -50,7 +51,7 @@ from selve.sensor import SelveSensor
50
51
  from selve.util import *
51
52
  from selve.util import Command
52
53
  from selve.util.errors import *
53
- from selve.util.protocol import ParameterType
54
+ from selve.util.protocol import ParameterType, SelveTypes, MovementState
54
55
  from selve.util.serial_transport import SerialTransport
55
56
 
56
57
 
@@ -105,11 +106,14 @@ class Selve:
105
106
  self._pending_futures = deque()
106
107
  self._event_queue = None
107
108
 
109
+ # Active movement polling tasks keyed by device id
110
+ self._movement_tasks: dict = {}
111
+
108
112
  #Options
109
113
  self.reversedStopPosition = 0
110
114
 
111
115
  #Logger
112
- self._LOGGER = logger
116
+ self._LOGGER: logging.Logger = logger or logging.getLogger(__name__)
113
117
 
114
118
 
115
119
  # Legacy worker was removed in favor of dedicated TX/RX tasks.
@@ -211,16 +215,19 @@ class Selve:
211
215
  await asyncio.sleep(5)
212
216
  self._LOGGER.debug("(Selve Worker): " + "Recovering")
213
217
 
218
+ # Tear down the broken transport, then rebuild it directly without calling
219
+ # _probe_port / stopWorker, which would kill the running TX/dispatch tasks.
214
220
  await self._teardown_transport()
215
221
 
216
222
  if self._port is not None:
217
223
  try:
218
- if await self._probe_port(self._port, fromConfigFlow=False):
219
- if self.rxQ is not None and self._transport is not None:
220
- await self._transport.start_reader(self.rxQ)
221
- return
224
+ await self._build_transport(self._port)
225
+ if self.rxQ is not None and self._transport is not None:
226
+ await self._transport.start_reader(self.rxQ)
227
+ self._LOGGER.info("(Selve Worker): Recovery successful on " + str(self._port))
228
+ return
222
229
  except (OSError, IOError) as e:
223
- self._LOGGER.debug("(Selve Worker): " + "Configured port not valid, maybe it has changed, trying other ports...")
230
+ self._LOGGER.debug("(Selve Worker): " + "Configured port not valid, maybe it has changed, trying other ports... " + str(e))
224
231
  except Exception as e:
225
232
  self._LOGGER.error("(Selve Worker): " + "Unknown exception: " + str(e))
226
233
 
@@ -235,10 +242,12 @@ class Selve:
235
242
 
236
243
  for p in available_ports:
237
244
  try:
238
- if await self._probe_port(p.device, fromConfigFlow=False):
239
- if self.rxQ is not None and self._transport is not None:
240
- await self._transport.start_reader(self.rxQ)
241
- return
245
+ await self._build_transport(p.device)
246
+ self._port = p.device
247
+ if self.rxQ is not None and self._transport is not None:
248
+ await self._transport.start_reader(self.rxQ)
249
+ self._LOGGER.info("(Selve Worker): Recovery successful on " + str(p.device))
250
+ return
242
251
  except Exception as e:
243
252
  self._LOGGER.error("(Selve Worker): " + "Error at com port: " + str(e))
244
253
  else:
@@ -1004,6 +1013,8 @@ class Selve:
1004
1013
  device.device_type = response.deviceType
1005
1014
 
1006
1015
  self.addOrUpdateDevice(device, SelveTypes.DEVICE)
1016
+ if device.state == MovementState.STOPPED_OFF:
1017
+ self._stop_movement_polling(device.id)
1007
1018
 
1008
1019
  if isinstance(response, SensorEventResponse):
1009
1020
  if self.is_id_registered(response.id, SelveTypes.SENSOR):
@@ -1057,20 +1068,39 @@ class Selve:
1057
1068
  callback(response)
1058
1069
 
1059
1070
 
1060
- def _handleCommandResult(self, response: IveoResultResponse | CommandResultResponse):
1061
-
1062
- # if isinstance(response, IveoResultResponse):
1063
- # for id in response.executedIds:
1064
- # dev = self.getDevice(id, SelveTypes.IVEO)
1065
-
1066
- # if response.command is DriveCommandIveo.DOWN:
1067
- # dev.state = MovementState.DOWN_ON
1068
- # if response.command is DriveCommandIveo.UP:
1069
- # dev.state = MovementState.UP_ON
1070
- # if response.command is DriveCommandIveo.STOP:
1071
- # dev.state = MovementState.STOPPED_OFF
1072
-
1073
- # self.addOrUpdateDevice(dev, SelveTypes.IVEO)
1071
+ def _handleCommandResult(self, response):
1072
+ if isinstance(response, IveoResultResponse):
1073
+ for id in response.executedIds:
1074
+ dev = self.getDevice(id, SelveTypes.IVEO)
1075
+ if dev is None:
1076
+ continue
1077
+ if response.command is DriveCommandIveo.DOWN:
1078
+ dev.state = MovementState.DOWN_ON
1079
+ elif response.command is DriveCommandIveo.UP:
1080
+ dev.state = MovementState.UP_ON
1081
+ elif response.command is DriveCommandIveo.STOP:
1082
+ dev.state = MovementState.STOPPED_OFF
1083
+ self.addOrUpdateDevice(dev, SelveTypes.IVEO)
1084
+
1085
+ elif isinstance(response, CommandResultResponse):
1086
+ for id in response.successIds:
1087
+ dev = self.getDevice(id, SelveTypes.DEVICE)
1088
+ if dev is None:
1089
+ continue
1090
+ dev.unreachable = False
1091
+ if response.command is DriveCommandCommeo.DRIVEDOWN:
1092
+ dev.state = MovementState.DOWN_ON
1093
+ elif response.command is DriveCommandCommeo.DRIVEUP:
1094
+ dev.state = MovementState.UP_ON
1095
+ elif response.command is DriveCommandCommeo.STOP:
1096
+ dev.state = MovementState.STOPPED_OFF
1097
+ self.addOrUpdateDevice(dev, SelveTypes.DEVICE)
1098
+ for id in response.failedIds:
1099
+ dev = self.getDevice(id, SelveTypes.DEVICE)
1100
+ if dev is None:
1101
+ continue
1102
+ dev.unreachable = True
1103
+ self.addOrUpdateDevice(dev, SelveTypes.DEVICE)
1074
1104
 
1075
1105
  for callback in self._callbacks:
1076
1106
  callback()
@@ -1350,7 +1380,7 @@ class Selve:
1350
1380
  else:
1351
1381
  dev.targetValue = 100 - response.targetValue if response.targetValue else 0
1352
1382
 
1353
- dev.unreachable = response.unreachable if response.unreachable else True
1383
+ dev.unreachable = response.unreachable
1354
1384
  dev.overload = response.overload if response.overload else False
1355
1385
  dev.obstructed = response.obstructed if response.obstructed else False
1356
1386
  dev.alarm = response.alarm if response.alarm else False
@@ -1362,6 +1392,8 @@ class Selve:
1362
1392
  dev.freezingAlarm = response.freezingAlarm if response.freezingAlarm else False
1363
1393
  dev.dayMode = response.dayMode if response.dayMode else False
1364
1394
  self.addOrUpdateDevice(dev, SelveTypes.DEVICE)
1395
+ if dev is not None and dev.state == MovementState.STOPPED_OFF:
1396
+ self._stop_movement_polling(id)
1365
1397
 
1366
1398
  def setDeviceValue(self, id: int, value: int, type: SelveTypes):
1367
1399
  dev = self.getDevice(id, type)
@@ -1385,12 +1417,41 @@ class Selve:
1385
1417
  dev.state = state
1386
1418
  self.addOrUpdateDevice(dev, type)
1387
1419
 
1420
+ def _start_movement_polling(self, device_id: int) -> None:
1421
+ """Start a background task that polls device values every 0.5 s during movement."""
1422
+ self._stop_movement_polling(device_id)
1423
+ try:
1424
+ asyncio.get_running_loop()
1425
+ except RuntimeError:
1426
+ return # no running event loop (unit tests, etc.)
1427
+ task = asyncio.create_task(self._movement_poll_loop(device_id))
1428
+ self._movement_tasks[device_id] = task
1429
+
1430
+ def _stop_movement_polling(self, device_id: int) -> None:
1431
+ """Cancel movement polling task for a device if one is active."""
1432
+ task = self._movement_tasks.pop(device_id, None)
1433
+ if task and not task.done():
1434
+ task.cancel()
1435
+
1436
+ async def _movement_poll_loop(self, device_id: int, interval: float = 0.5, timeout: float = 60.0) -> None:
1437
+ """Poll DeviceGetValues every *interval* seconds until movement stops or timeout."""
1438
+ elapsed = 0.0
1439
+ while not self._stopThread.is_set() and elapsed < timeout:
1440
+ await asyncio.sleep(interval)
1441
+ elapsed += interval
1442
+ dev = self.getDevice(device_id, SelveTypes.DEVICE)
1443
+ if dev is None or dev.state == MovementState.STOPPED_OFF:
1444
+ break
1445
+ await self.updateCommeoDeviceValuesAsync(device_id)
1446
+ self._movement_tasks.pop(device_id, None)
1447
+
1388
1448
  async def moveDeviceUp(self, device: SelveDevice | IveoDevice, type=DeviceCommandType.MANUAL):
1389
1449
  if device.communicationType is CommunicationType.COMMEO:
1390
1450
  await self.executeCommand(CommandDriveUp(device.id, type))
1391
1451
  device.state = MovementState.UP_ON
1392
1452
  self.addOrUpdateDevice(device, SelveTypes.DEVICE)
1393
1453
  await self.updateCommeoDeviceValuesAsync(device.id)
1454
+ self._start_movement_polling(device.id)
1394
1455
  else:
1395
1456
  self.setDeviceState(device.id, MovementState.UP_ON, SelveTypes.IVEO)
1396
1457
  await self.executeCommand(IveoManual(device.id, DriveCommandIveo.UP))
@@ -1404,6 +1465,7 @@ class Selve:
1404
1465
  device.state = MovementState.DOWN_ON
1405
1466
  self.addOrUpdateDevice(device, SelveTypes.DEVICE)
1406
1467
  await self.updateCommeoDeviceValuesAsync(device.id)
1468
+ self._start_movement_polling(device.id)
1407
1469
  else:
1408
1470
  self.setDeviceState(device.id, MovementState.DOWN_ON, SelveTypes.IVEO)
1409
1471
  await self.executeCommand(IveoManual(device.id, DriveCommandIveo.DOWN))
@@ -1415,6 +1477,7 @@ class Selve:
1415
1477
  if device.communicationType is CommunicationType.COMMEO:
1416
1478
  await self.executeCommand(CommandDrivePos1(device.id, type))
1417
1479
  await self.updateCommeoDeviceValuesAsync(device.id)
1480
+ self._start_movement_polling(device.id)
1418
1481
  else:
1419
1482
  self.setDeviceState(device.id, MovementState.UP_ON, SelveTypes.IVEO)
1420
1483
  await self.executeCommand(IveoManual(device.id, DriveCommandIveo.POS1))
@@ -1426,6 +1489,7 @@ class Selve:
1426
1489
  if device.communicationType is CommunicationType.COMMEO:
1427
1490
  await self.executeCommand(CommandDrivePos2(device.id, type))
1428
1491
  await self.updateCommeoDeviceValuesAsync(device.id)
1492
+ self._start_movement_polling(device.id)
1429
1493
  else:
1430
1494
  self.setDeviceState(device.id, MovementState.DOWN_ON, SelveTypes.IVEO)
1431
1495
  await self.executeCommand(IveoManual(device.id, DriveCommandIveo.POS2))
@@ -1436,17 +1500,21 @@ class Selve:
1436
1500
  async def moveDevicePos(self, device: SelveDevice, pos: int = 0, type=DeviceCommandType.MANUAL):
1437
1501
  await self.executeCommand(CommandDrivePos(device.id, type, param=Util.percentageToValue(pos)))
1438
1502
  await self.updateCommeoDeviceValuesAsync(device.id)
1503
+ self._start_movement_polling(device.id)
1439
1504
 
1440
1505
  async def moveDeviceStepUp(self, device: SelveDevice, degrees: int = 0, type=DeviceCommandType.MANUAL):
1441
1506
  await self.executeCommand(CommandDriveStepUp(device.id, type, param=Util.degreesToValue(degrees)))
1442
1507
  await self.updateCommeoDeviceValuesAsync(device.id)
1508
+ self._start_movement_polling(device.id)
1443
1509
 
1444
1510
  async def moveDeviceStepDown(self, device: SelveDevice, degrees: int = 0, type=DeviceCommandType.MANUAL):
1445
1511
  await self.executeCommand(CommandDriveStepDown(device.id, type, param=Util.degreesToValue(degrees)))
1446
1512
  await self.updateCommeoDeviceValuesAsync(device.id)
1513
+ self._start_movement_polling(device.id)
1447
1514
 
1448
1515
  async def stopDevice(self, device: SelveDevice | IveoDevice, type=DeviceCommandType.MANUAL):
1449
1516
  if device.communicationType is CommunicationType.COMMEO:
1517
+ self._stop_movement_polling(device.id)
1450
1518
  await self.executeCommand(CommandStop(device.id, type))
1451
1519
  await self.updateCommeoDeviceValuesAsync(device.id)
1452
1520
  else:
@@ -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.2'
22
- __version_tuple__ = version_tuple = (2, 5, 2)
21
+ __version__ = version = '2.5.4'
22
+ __version_tuple__ = version_tuple = (2, 5, 4)
23
23
 
24
- __commit_id__ = commit_id = 'g24c865c13'
24
+ __commit_id__ = commit_id = 'g5d6793713'
@@ -90,6 +90,6 @@ class TestSelveGatewayIntegration:
90
90
  await mock_selve_instance.recover()
91
91
 
92
92
  # Verify logger was called (indicating recover ran)
93
- mock_logger.info.assert_called_with("(Selve Worker): Recover serial connection")
93
+ mock_logger.info.assert_any_call("(Selve Worker): Recover serial connection")
94
94
  mock_logger.debug.assert_any_call("(Selve Worker): Waiting 5 seconds before trying...")
95
95
  mock_logger.debug.assert_any_call("(Selve Worker): Recovering")
@@ -18,8 +18,9 @@ class TestSelveEdgeCases:
18
18
  def test_init_with_none_logger(self):
19
19
  """Test initialization with None logger."""
20
20
  selve = Selve(logger=None)
21
- # None logger should result in None _LOGGER (implementation allows this)
22
- assert selve._LOGGER is None
21
+ # None logger falls back to a module-level logger, never stays None
22
+ import logging
23
+ assert isinstance(selve._LOGGER, logging.Logger)
23
24
 
24
25
  def test_init_with_invalid_port(self):
25
26
  """Test initialization with invalid port."""
@@ -60,7 +60,8 @@ class TestSelveInit:
60
60
  selve = Selve()
61
61
 
62
62
  assert selve._port is None
63
- assert selve._LOGGER is None
63
+ import logging
64
+ assert isinstance(selve._LOGGER, logging.Logger)
64
65
  assert selve.loop is None
65
66
  assert isinstance(selve._callbacks, set)
66
67
  assert isinstance(selve._eventCallbacks, set)