python-selve-new 2.5.1__tar.gz → 2.5.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_selve_new-2.5.3/.github/workflows/python-publish.yml +45 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.github/workflows/tests.yml +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/PKG-INFO +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/pyproject.toml +1 -1
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/python_selve_new.egg-info/PKG-INFO +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/python_selve_new.egg-info/requires.txt +1 -1
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/__init__.py +86 -75
- python_selve_new-2.5.3/selve/_version.py +24 -0
- python_selve_new-2.5.3/selve/util/serial_transport.py +121 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/setup.py +1 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/conftest.py +22 -6
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/conftest.py +11 -11
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/test_device_integration.py +12 -12
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/test_selve_gateway_integration.py +3 -12
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/test_selve_integration.py +19 -32
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/test_replacement.py +16 -17
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_gateway_configuration_issues.py +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_gateway_error_handling_fixed.py +6 -8
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_missing_components.py +18 -20
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_mock_commands.py +23 -25
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_mock_devices_and_groups.py +28 -30
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_mock_sensors_and_senders.py +25 -27
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_port_discovery.py +5 -5
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_advanced_coverage.py +5 -9
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_core_coverage.py +17 -99
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_edge_cases.py +3 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_gateway.py +8 -10
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_init_comprehensive.py +22 -24
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_init_simple.py +9 -13
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_main_class_extensive.py +13 -26
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_service_command_errors.py +2 -2
- python_selve_new-2.5.1/.github/workflows/python-publish.yml +0 -100
- python_selve_new-2.5.1/selve/_version.py +0 -34
- python_selve_new-2.5.1/selve/util/serial_transport.py +0 -129
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.github/FUNDING.yml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.github/architect.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.github/ask.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.github/code.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.github/debug.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.gitignore +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.idea/.gitignore +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.idea/misc.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.idea/modules.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.idea/python-selve-new.iml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/.idea/vcs.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/CHANGELOG.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/LICENSE +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/README.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/coverage_xdist_example.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/debug_response.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/debug_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/direct_hardware_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/direct_hardware_test.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/generate_coverage.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/package.sh +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/python_selve_new.egg-info/SOURCES.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/python_selve_new.egg-info/dependency_links.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/python_selve_new.egg-info/top_level.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/release.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/run_all_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/run_hardware_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/run_integration_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/run_mock_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/run_single_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/run_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/command.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/device.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/event.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/firmware.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/group.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/iveo.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/param.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/senSim.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/sender.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/sensor.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/commands/service.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/device.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/gateway.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/group.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/iveo.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/senSim.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/sender.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/sensor.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/util/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/util/errors.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/selve/util/protocol.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/setup.cfg +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/setup_and_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/README.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/integration/test_selve_hardware.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/test_import.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/mock_utils.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_command_coverage.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_commands.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_device.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_device_classes_coverage.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_device_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_group.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_group_commands.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_param_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_selve_init_response_coverage.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_sender_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_sensim_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_sensor_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_service_commands.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_util.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.3}/tests/unit/test_utility_coverage.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Publishes to PyPI after the "Run Tests" workflow succeeds on a version tag.
|
|
2
|
+
|
|
3
|
+
name: Publish Python Package
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
workflow_run:
|
|
7
|
+
workflows: ["Run Tests"]
|
|
8
|
+
types: [completed]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
deploy:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
# Only publish when tests passed AND the trigger was a version tag (e.g. v2.5.2)
|
|
17
|
+
if: |
|
|
18
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
19
|
+
startsWith(github.event.workflow_run.head_branch, 'v')
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0 # Fetch all history for setuptools_scm
|
|
25
|
+
ref: ${{ github.event.workflow_run.head_sha }}
|
|
26
|
+
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: '3.x'
|
|
31
|
+
|
|
32
|
+
- name: Install build dependencies
|
|
33
|
+
run: |
|
|
34
|
+
python -m pip install --upgrade pip
|
|
35
|
+
pip install build setuptools-scm
|
|
36
|
+
|
|
37
|
+
- name: Build package
|
|
38
|
+
run: |
|
|
39
|
+
python -m build
|
|
40
|
+
|
|
41
|
+
- name: Publish package to PyPI
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
|
43
|
+
with:
|
|
44
|
+
user: __token__
|
|
45
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
strategy:
|
|
17
17
|
matrix:
|
|
18
|
-
python-version: ['3.
|
|
18
|
+
python-version: ['3.10', '3.11', '3.12', '3.13']
|
|
19
19
|
|
|
20
20
|
steps:
|
|
21
21
|
- uses: actions/checkout@v4
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
- name: Run integration tests (without hardware)
|
|
50
50
|
run: |
|
|
51
|
-
pytest tests/integration/ -v -m "not hardware" --timeout=30
|
|
51
|
+
pytest tests/integration/ -v -m "not hardware" --timeout=30 --ignore=tests/integration/test_selve_hardware.py
|
|
52
52
|
|
|
53
53
|
- name: Generate coverage report (Python 3.11 only)
|
|
54
54
|
if: matrix.python-version == '3.11'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-selve-new
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.3
|
|
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
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: requests
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: serialx
|
|
25
25
|
Requires-Dist: pybase64
|
|
26
26
|
Requires-Dist: untangle
|
|
27
27
|
Requires-Dist: nest_asyncio
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-selve-new
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.3
|
|
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
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: requests
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: serialx
|
|
25
25
|
Requires-Dist: pybase64
|
|
26
26
|
Requires-Dist: untangle
|
|
27
27
|
Requires-Dist: nest_asyncio
|
|
@@ -12,15 +12,20 @@ except ImportError:
|
|
|
12
12
|
__version__ = "unknown"
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
-
import
|
|
15
|
+
import logging
|
|
16
16
|
import time
|
|
17
17
|
from collections import deque
|
|
18
18
|
from itertools import chain
|
|
19
19
|
from typing import Callable, Optional
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
from serial.tools import list_ports
|
|
23
|
-
|
|
21
|
+
try:
|
|
22
|
+
from serial.tools import list_ports as _serial_list_ports
|
|
23
|
+
def _comports():
|
|
24
|
+
return _serial_list_ports.comports()
|
|
25
|
+
except ImportError:
|
|
26
|
+
def _comports(): # type: ignore[misc]
|
|
27
|
+
return []
|
|
28
|
+
|
|
24
29
|
import untangle
|
|
25
30
|
|
|
26
31
|
from selve.commands import param, service
|
|
@@ -105,7 +110,7 @@ class Selve:
|
|
|
105
110
|
self.reversedStopPosition = 0
|
|
106
111
|
|
|
107
112
|
#Logger
|
|
108
|
-
self._LOGGER = logger
|
|
113
|
+
self._LOGGER: logging.Logger = logger or logging.getLogger(__name__)
|
|
109
114
|
|
|
110
115
|
|
|
111
116
|
# Legacy worker was removed in favor of dedicated TX/RX tasks.
|
|
@@ -113,21 +118,21 @@ class Selve:
|
|
|
113
118
|
# Kept for backward compatibility in tests/mocks.
|
|
114
119
|
return True
|
|
115
120
|
|
|
116
|
-
def _build_transport(self, port: str):
|
|
121
|
+
async def _build_transport(self, port: str):
|
|
117
122
|
self._transport = SerialTransport(port=port, logger=self._LOGGER)
|
|
118
|
-
self._transport.ensure_open()
|
|
119
|
-
self._serial =
|
|
123
|
+
await self._transport.ensure_open()
|
|
124
|
+
self._serial = None
|
|
120
125
|
|
|
121
|
-
def _teardown_transport(self):
|
|
126
|
+
async def _teardown_transport(self):
|
|
122
127
|
if self._transport:
|
|
123
|
-
self._transport.shutdown()
|
|
128
|
+
await self._transport.shutdown()
|
|
124
129
|
self._transport = None
|
|
125
130
|
self._serial = None
|
|
126
131
|
|
|
127
132
|
async def _probe_port(self, port: str, fromConfigFlow: bool = False) -> bool:
|
|
128
133
|
"""Attempt to connect and verify a Selve gateway on the given port."""
|
|
129
134
|
try:
|
|
130
|
-
self._build_transport(port)
|
|
135
|
+
await self._build_transport(port)
|
|
131
136
|
ok = await self.pingGateway(fromConfigFlow=fromConfigFlow)
|
|
132
137
|
if ok:
|
|
133
138
|
try:
|
|
@@ -142,13 +147,12 @@ class Selve:
|
|
|
142
147
|
self._LOGGER.debug(f"Probe failed on {port}: {e}")
|
|
143
148
|
|
|
144
149
|
await self.stopWorker()
|
|
145
|
-
self._teardown_transport()
|
|
150
|
+
await self._teardown_transport()
|
|
146
151
|
return False
|
|
147
152
|
|
|
148
153
|
|
|
149
154
|
def list_ports(self):
|
|
150
|
-
|
|
151
|
-
return available_ports
|
|
155
|
+
return _comports()
|
|
152
156
|
|
|
153
157
|
async def check_port(self, port):
|
|
154
158
|
if port is not None:
|
|
@@ -172,22 +176,14 @@ class Selve:
|
|
|
172
176
|
await self.discover()
|
|
173
177
|
await self.startWorker()
|
|
174
178
|
return
|
|
175
|
-
except (
|
|
179
|
+
except (OSError, IOError) as e:
|
|
176
180
|
self._LOGGER.debug("Configured port not valid! " + str(e))
|
|
177
181
|
except Exception as e:
|
|
178
182
|
self._LOGGER.error("Unknown exception: " + str(e))
|
|
179
183
|
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
try:
|
|
184
|
-
current_loop = asyncio.get_running_loop()
|
|
185
|
-
available_ports = await current_loop.run_in_executor(None, list_ports.comports)
|
|
186
|
-
except RuntimeError:
|
|
187
|
-
# No running loop, use the instance loop
|
|
188
|
-
available_ports = await self.loop.run_in_executor(None, list_ports.comports)
|
|
189
|
-
else:
|
|
190
|
-
available_ports = list_ports.comports()
|
|
185
|
+
loop = asyncio.get_running_loop()
|
|
186
|
+
available_ports = await loop.run_in_executor(None, _comports)
|
|
191
187
|
|
|
192
188
|
self._LOGGER.debug("available comports: " + str(available_ports))
|
|
193
189
|
|
|
@@ -216,31 +212,25 @@ class Selve:
|
|
|
216
212
|
await asyncio.sleep(5)
|
|
217
213
|
self._LOGGER.debug("(Selve Worker): " + "Recovering")
|
|
218
214
|
|
|
219
|
-
|
|
215
|
+
# Tear down the broken transport, then rebuild it directly without calling
|
|
216
|
+
# _probe_port / stopWorker, which would kill the running TX/dispatch tasks.
|
|
217
|
+
await self._teardown_transport()
|
|
220
218
|
|
|
221
219
|
if self._port is not None:
|
|
222
220
|
try:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
except (
|
|
229
|
-
self._LOGGER.debug("(Selve Worker): " + "Configured port not valid, maybe it has changed, trying other ports...")
|
|
221
|
+
await self._build_transport(self._port)
|
|
222
|
+
if self.rxQ is not None and self._transport is not None:
|
|
223
|
+
await self._transport.start_reader(self.rxQ)
|
|
224
|
+
self._LOGGER.info("(Selve Worker): Recovery successful on " + str(self._port))
|
|
225
|
+
return
|
|
226
|
+
except (OSError, IOError) as e:
|
|
227
|
+
self._LOGGER.debug("(Selve Worker): " + "Configured port not valid, maybe it has changed, trying other ports... " + str(e))
|
|
230
228
|
except Exception as e:
|
|
231
229
|
self._LOGGER.error("(Selve Worker): " + "Unknown exception: " + str(e))
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
current_loop = asyncio.get_running_loop()
|
|
237
|
-
available_ports = await current_loop.run_in_executor(None, list_ports.comports)
|
|
238
|
-
except RuntimeError:
|
|
239
|
-
# No running loop, use the instance loop
|
|
240
|
-
available_ports = await self.loop.run_in_executor(None, list_ports.comports)
|
|
241
|
-
else:
|
|
242
|
-
available_ports = list_ports.comports()
|
|
243
|
-
|
|
231
|
+
loop = asyncio.get_running_loop()
|
|
232
|
+
available_ports = await loop.run_in_executor(None, _comports)
|
|
233
|
+
|
|
244
234
|
self._LOGGER.debug("(Selve Worker): " + "available comports: " + str(available_ports))
|
|
245
235
|
|
|
246
236
|
if len(available_ports) == 0:
|
|
@@ -249,11 +239,12 @@ class Selve:
|
|
|
249
239
|
|
|
250
240
|
for p in available_ports:
|
|
251
241
|
try:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
242
|
+
await self._build_transport(p.device)
|
|
243
|
+
self._port = p.device
|
|
244
|
+
if self.rxQ is not None and self._transport is not None:
|
|
245
|
+
await self._transport.start_reader(self.rxQ)
|
|
246
|
+
self._LOGGER.info("(Selve Worker): Recovery successful on " + str(p.device))
|
|
247
|
+
return
|
|
257
248
|
except Exception as e:
|
|
258
249
|
self._LOGGER.error("(Selve Worker): " + "Error at com port: " + str(e))
|
|
259
250
|
else:
|
|
@@ -273,12 +264,11 @@ class Selve:
|
|
|
273
264
|
if self._event_queue is None or not isinstance(self._event_queue, asyncio.Queue):
|
|
274
265
|
self._event_queue = asyncio.Queue()
|
|
275
266
|
|
|
276
|
-
# Ensure transport and reader
|
|
277
|
-
loop = self.loop or asyncio.get_running_loop()
|
|
267
|
+
# Ensure transport and reader task are running
|
|
278
268
|
if self._transport is None and self._port is not None:
|
|
279
|
-
self._build_transport(self._port)
|
|
269
|
+
await self._build_transport(self._port)
|
|
280
270
|
if self._transport is not None:
|
|
281
|
-
self._transport.start_reader(
|
|
271
|
+
await self._transport.start_reader(self.rxQ)
|
|
282
272
|
|
|
283
273
|
if self._tx_task is None or self._tx_task.done():
|
|
284
274
|
self._tx_task = asyncio.create_task(self._tx_loop())
|
|
@@ -371,7 +361,7 @@ class Selve:
|
|
|
371
361
|
self._dispatch_task = None
|
|
372
362
|
self._pending_futures.clear()
|
|
373
363
|
if self._transport:
|
|
374
|
-
self._transport.stop_reader()
|
|
364
|
+
await self._transport.stop_reader()
|
|
375
365
|
if self._event_queue is not None:
|
|
376
366
|
while not self._event_queue.empty():
|
|
377
367
|
try:
|
|
@@ -387,7 +377,7 @@ class Selve:
|
|
|
387
377
|
self._LOGGER.debug("Preparing for termination")
|
|
388
378
|
await self.stopWorker()
|
|
389
379
|
# close the serial port, do the cleanup
|
|
390
|
-
self._teardown_transport()
|
|
380
|
+
await self._teardown_transport()
|
|
391
381
|
return True
|
|
392
382
|
|
|
393
383
|
|
|
@@ -428,20 +418,20 @@ class Selve:
|
|
|
428
418
|
if self._transport is None:
|
|
429
419
|
if self._port is None:
|
|
430
420
|
raise PortError("No serial port configured")
|
|
431
|
-
self._build_transport(self._port)
|
|
421
|
+
await self._build_transport(self._port)
|
|
432
422
|
|
|
433
423
|
await self._transport.write(commandstr)
|
|
434
424
|
# small pause to give the gateway time to answer
|
|
435
425
|
await asyncio.sleep(0.1)
|
|
436
426
|
|
|
437
|
-
except (
|
|
427
|
+
except (OSError, IOError) as se:
|
|
438
428
|
self._LOGGER.info('Serial error, trying to reconnect once... ' + str(se))
|
|
439
429
|
await self.recover()
|
|
440
430
|
|
|
441
431
|
try:
|
|
442
432
|
self._LOGGER.debug('Trying again...')
|
|
443
433
|
if self._transport is None and self._port is not None:
|
|
444
|
-
self._build_transport(self._port)
|
|
434
|
+
await self._build_transport(self._port)
|
|
445
435
|
await self._transport.write(commandstr)
|
|
446
436
|
await asyncio.sleep(0.1)
|
|
447
437
|
|
|
@@ -1073,20 +1063,39 @@ class Selve:
|
|
|
1073
1063
|
callback(response)
|
|
1074
1064
|
|
|
1075
1065
|
|
|
1076
|
-
def _handleCommandResult(self, response
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1066
|
+
def _handleCommandResult(self, response):
|
|
1067
|
+
if isinstance(response, IveoResultResponse):
|
|
1068
|
+
for id in response.executedIds:
|
|
1069
|
+
dev = self.getDevice(id, SelveTypes.IVEO)
|
|
1070
|
+
if dev is None:
|
|
1071
|
+
continue
|
|
1072
|
+
if response.command is DriveCommandIveo.DOWN:
|
|
1073
|
+
dev.state = MovementState.DOWN_ON
|
|
1074
|
+
elif response.command is DriveCommandIveo.UP:
|
|
1075
|
+
dev.state = MovementState.UP_ON
|
|
1076
|
+
elif response.command is DriveCommandIveo.STOP:
|
|
1077
|
+
dev.state = MovementState.STOPPED_OFF
|
|
1078
|
+
self.addOrUpdateDevice(dev, SelveTypes.IVEO)
|
|
1079
|
+
|
|
1080
|
+
elif isinstance(response, CommandResultResponse):
|
|
1081
|
+
for id in response.successIds:
|
|
1082
|
+
dev = self.getDevice(id, SelveTypes.DEVICE)
|
|
1083
|
+
if dev is None:
|
|
1084
|
+
continue
|
|
1085
|
+
dev.unreachable = False
|
|
1086
|
+
if response.command is DriveCommandCommeo.DRIVEDOWN:
|
|
1087
|
+
dev.state = MovementState.DOWN_ON
|
|
1088
|
+
elif response.command is DriveCommandCommeo.DRIVEUP:
|
|
1089
|
+
dev.state = MovementState.UP_ON
|
|
1090
|
+
elif response.command is DriveCommandCommeo.STOP:
|
|
1091
|
+
dev.state = MovementState.STOPPED_OFF
|
|
1092
|
+
self.addOrUpdateDevice(dev, SelveTypes.DEVICE)
|
|
1093
|
+
for id in response.failedIds:
|
|
1094
|
+
dev = self.getDevice(id, SelveTypes.DEVICE)
|
|
1095
|
+
if dev is None:
|
|
1096
|
+
continue
|
|
1097
|
+
dev.unreachable = True
|
|
1098
|
+
self.addOrUpdateDevice(dev, SelveTypes.DEVICE)
|
|
1090
1099
|
|
|
1091
1100
|
for callback in self._callbacks:
|
|
1092
1101
|
callback()
|
|
@@ -1188,7 +1197,8 @@ class Selve:
|
|
|
1188
1197
|
while await self.gatewayState() != ServiceState.READY:
|
|
1189
1198
|
if time.time() - start_time >= 30:
|
|
1190
1199
|
self._LOGGER.info("Error: Gateway could not be reset or loads too long")
|
|
1191
|
-
|
|
1200
|
+
break
|
|
1201
|
+
await asyncio.sleep(0.1)
|
|
1192
1202
|
self._LOGGER.info("Gateway reset")
|
|
1193
1203
|
|
|
1194
1204
|
async def factoryResetGateway(self):
|
|
@@ -1201,7 +1211,8 @@ class Selve:
|
|
|
1201
1211
|
while await self.gatewayState() != ServiceState.READY:
|
|
1202
1212
|
if time.time() - start_time >= 60:
|
|
1203
1213
|
self._LOGGER.info("Error: Gateway could not be reset or loads too long")
|
|
1204
|
-
|
|
1214
|
+
break
|
|
1215
|
+
await asyncio.sleep(0.1)
|
|
1205
1216
|
self._LOGGER.info("Gateway factory reset")
|
|
1206
1217
|
return response.executed
|
|
1207
1218
|
|
|
@@ -1364,7 +1375,7 @@ class Selve:
|
|
|
1364
1375
|
else:
|
|
1365
1376
|
dev.targetValue = 100 - response.targetValue if response.targetValue else 0
|
|
1366
1377
|
|
|
1367
|
-
dev.unreachable = response.unreachable
|
|
1378
|
+
dev.unreachable = response.unreachable
|
|
1368
1379
|
dev.overload = response.overload if response.overload else False
|
|
1369
1380
|
dev.obstructed = response.obstructed if response.obstructed else False
|
|
1370
1381
|
dev.alarm = response.alarm if response.alarm else False
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '2.5.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 5, 3)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'ge708968d7'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import serialx
|
|
6
|
+
from serialx import Parity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SerialTransport:
|
|
10
|
+
"""Async serial transport using serialx (replaces pyserial + background thread)."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
port: str,
|
|
15
|
+
baudrate: int = 115200,
|
|
16
|
+
read_timeout: float = 1.0,
|
|
17
|
+
logger: Optional[logging.Logger] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self._port = port
|
|
20
|
+
self._baudrate = baudrate
|
|
21
|
+
self._read_timeout = read_timeout
|
|
22
|
+
self._logger = logger or logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
self._reader: Optional[asyncio.StreamReader] = None
|
|
25
|
+
self._writer: Optional[asyncio.StreamWriter] = None
|
|
26
|
+
self._reader_task: Optional[asyncio.Task] = None
|
|
27
|
+
self._rx_queue: Optional[asyncio.Queue] = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def port(self) -> str:
|
|
31
|
+
return self._port
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_open(self) -> bool:
|
|
35
|
+
return self._writer is not None and not self._writer.is_closing()
|
|
36
|
+
|
|
37
|
+
async def ensure_open(self) -> None:
|
|
38
|
+
if not self.is_open:
|
|
39
|
+
self._reader, self._writer = await serialx.open_serial_connection(
|
|
40
|
+
url=self._port,
|
|
41
|
+
baudrate=self._baudrate,
|
|
42
|
+
bytesize=8,
|
|
43
|
+
parity=Parity.NONE,
|
|
44
|
+
stopbits=1,
|
|
45
|
+
xonxoff=False,
|
|
46
|
+
rtscts=False,
|
|
47
|
+
dsrdtr=False,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
try:
|
|
52
|
+
if self._writer and not self._writer.is_closing():
|
|
53
|
+
self._writer.close()
|
|
54
|
+
await self._writer.wait_closed()
|
|
55
|
+
except Exception:
|
|
56
|
+
self._logger.debug("Serial close failed", exc_info=True)
|
|
57
|
+
self._reader = None
|
|
58
|
+
self._writer = None
|
|
59
|
+
|
|
60
|
+
async def start_reader(self, rx_queue: asyncio.Queue) -> None:
|
|
61
|
+
"""Opens the serial port and starts the async reader task."""
|
|
62
|
+
await self.ensure_open()
|
|
63
|
+
self._rx_queue = rx_queue
|
|
64
|
+
if self._reader_task and not self._reader_task.done():
|
|
65
|
+
return
|
|
66
|
+
self._reader_task = asyncio.create_task(
|
|
67
|
+
self._reader_loop(), name="selve-serial-reader"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def stop_reader(self) -> None:
|
|
71
|
+
if self._reader_task:
|
|
72
|
+
self._reader_task.cancel()
|
|
73
|
+
try:
|
|
74
|
+
await self._reader_task
|
|
75
|
+
except asyncio.CancelledError:
|
|
76
|
+
pass
|
|
77
|
+
self._reader_task = None
|
|
78
|
+
|
|
79
|
+
async def _reader_loop(self) -> None:
|
|
80
|
+
buffer = ""
|
|
81
|
+
while True:
|
|
82
|
+
try:
|
|
83
|
+
if self._reader is None:
|
|
84
|
+
await self.ensure_open()
|
|
85
|
+
line_bytes = await self._reader.readline()
|
|
86
|
+
except asyncio.CancelledError:
|
|
87
|
+
break
|
|
88
|
+
except (OSError, EOFError) as exc:
|
|
89
|
+
self._logger.error("Serial read error: %s", exc)
|
|
90
|
+
await asyncio.sleep(1)
|
|
91
|
+
continue
|
|
92
|
+
except Exception:
|
|
93
|
+
self._logger.error("Unexpected serial read error", exc_info=True)
|
|
94
|
+
await asyncio.sleep(1)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if not line_bytes:
|
|
98
|
+
await asyncio.sleep(0.01)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
decoded = line_bytes.decode(errors="ignore").strip()
|
|
102
|
+
if decoded == "":
|
|
103
|
+
if buffer and self._rx_queue:
|
|
104
|
+
await self._rx_queue.put(buffer)
|
|
105
|
+
buffer = ""
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
buffer += decoded
|
|
109
|
+
|
|
110
|
+
async def write(self, payload: bytes) -> None:
|
|
111
|
+
await self.ensure_open()
|
|
112
|
+
try:
|
|
113
|
+
self._writer.write(payload)
|
|
114
|
+
await self._writer.drain()
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
self._logger.error("Serial write error: %s", exc)
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
async def shutdown(self) -> None:
|
|
120
|
+
await self.stop_reader()
|
|
121
|
+
await self.close()
|
|
@@ -39,7 +39,6 @@ setup(
|
|
|
39
39
|
# Specify the Python versions you support here. In particular, ensure
|
|
40
40
|
# that you indicate whether you support Python 2, Python 3 or both.
|
|
41
41
|
'Programming Language :: Python :: 3',
|
|
42
|
-
'Programming Language :: Python :: 3.9',
|
|
43
42
|
'Programming Language :: Python :: 3.10',
|
|
44
43
|
'Programming Language :: Python :: 3.11',
|
|
45
44
|
'Programming Language :: Python :: 3.12'
|
|
@@ -56,7 +55,7 @@ setup(
|
|
|
56
55
|
# For an analysis of "install_requires" vs pip's requirements files see:
|
|
57
56
|
# https://packaging.python.org/en/latest/requirements.html
|
|
58
57
|
install_requires=[
|
|
59
|
-
'
|
|
58
|
+
'serialx',
|
|
60
59
|
'pybase64',
|
|
61
60
|
'untangle',
|
|
62
61
|
'nest_asyncio',
|
|
@@ -3,7 +3,19 @@ import logging
|
|
|
3
3
|
import sys
|
|
4
4
|
import asyncio
|
|
5
5
|
import nest_asyncio
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
7
|
+
|
|
8
|
+
# Stub out serialx before any selve import so the protobuf/aioesphomeapi
|
|
9
|
+
# version conflict in serialx's optional esphome platform never fires.
|
|
10
|
+
_serialx_mock = MagicMock()
|
|
11
|
+
_serialx_mock.Parity = MagicMock()
|
|
12
|
+
_serialx_mock.Parity.NONE = "N"
|
|
13
|
+
_serialx_mock.open_serial_connection = AsyncMock(
|
|
14
|
+
return_value=(MagicMock(), MagicMock())
|
|
15
|
+
)
|
|
16
|
+
sys.modules.setdefault("serialx", _serialx_mock)
|
|
17
|
+
sys.modules.setdefault("serialx.platforms", MagicMock())
|
|
18
|
+
sys.modules.setdefault("serialx.platforms.serial_esphome", MagicMock())
|
|
7
19
|
|
|
8
20
|
# Ermöglicht das Ausführen von async-Funktionen in einem bereits laufenden Event-Loop
|
|
9
21
|
# Dies ist wichtig für die Integration-Tests
|
|
@@ -34,19 +46,23 @@ def logger():
|
|
|
34
46
|
|
|
35
47
|
@pytest.fixture
|
|
36
48
|
def mock_serial():
|
|
37
|
-
"""Mock the
|
|
38
|
-
with patch('selve.
|
|
49
|
+
"""Mock the SerialTransport for testing."""
|
|
50
|
+
with patch('selve.util.serial_transport.SerialTransport') as mock:
|
|
39
51
|
mock_instance = MagicMock()
|
|
40
52
|
mock.return_value = mock_instance
|
|
41
53
|
mock_instance.is_open = True
|
|
42
|
-
mock_instance.
|
|
54
|
+
mock_instance.ensure_open = AsyncMock()
|
|
55
|
+
mock_instance.start_reader = AsyncMock()
|
|
56
|
+
mock_instance.stop_reader = AsyncMock()
|
|
57
|
+
mock_instance.write = AsyncMock()
|
|
58
|
+
mock_instance.shutdown = AsyncMock()
|
|
43
59
|
yield mock
|
|
44
60
|
|
|
45
61
|
|
|
46
62
|
@pytest.fixture
|
|
47
63
|
def mock_list_ports():
|
|
48
|
-
"""Mock the
|
|
49
|
-
with patch('selve.
|
|
64
|
+
"""Mock the _comports function."""
|
|
65
|
+
with patch('selve._comports') as mock:
|
|
50
66
|
# Create a mock port
|
|
51
67
|
mock_port = MagicMock()
|
|
52
68
|
mock_port.device = "COM3"
|
|
@@ -39,17 +39,17 @@ def event_loop():
|
|
|
39
39
|
|
|
40
40
|
@pytest.fixture
|
|
41
41
|
def mock_serial():
|
|
42
|
-
"""Provide a mocked serial
|
|
43
|
-
with patch('selve.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
yield
|
|
42
|
+
"""Provide a mocked serial transport."""
|
|
43
|
+
with patch('selve.util.serial_transport.SerialTransport') as mock_transport:
|
|
44
|
+
mock_instance = mock_transport.return_value
|
|
45
|
+
mock_instance.is_open = True
|
|
46
|
+
mock_instance.ensure_open = AsyncMock()
|
|
47
|
+
mock_instance.start_reader = AsyncMock()
|
|
48
|
+
mock_instance.stop_reader = AsyncMock()
|
|
49
|
+
mock_instance.write = AsyncMock()
|
|
50
|
+
mock_instance.shutdown = AsyncMock()
|
|
51
|
+
|
|
52
|
+
yield mock_transport
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
@pytest.fixture
|