python-selve-new 2.5.1__tar.gz → 2.5.2__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.2/.github/workflows/python-publish.yml +45 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.github/workflows/tests.yml +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/PKG-INFO +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/pyproject.toml +1 -1
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/python_selve_new.egg-info/PKG-INFO +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/python_selve_new.egg-info/requires.txt +1 -1
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/__init__.py +38 -52
- python_selve_new-2.5.2/selve/_version.py +24 -0
- python_selve_new-2.5.2/selve/util/serial_transport.py +121 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/setup.py +1 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/conftest.py +22 -6
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/conftest.py +11 -11
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/test_device_integration.py +12 -12
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/test_selve_gateway_integration.py +2 -11
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/test_selve_integration.py +19 -32
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/test_replacement.py +16 -17
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_gateway_configuration_issues.py +2 -2
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_gateway_error_handling_fixed.py +6 -8
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_missing_components.py +18 -20
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_mock_commands.py +23 -25
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_mock_devices_and_groups.py +28 -30
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_mock_sensors_and_senders.py +25 -27
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_port_discovery.py +5 -5
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_advanced_coverage.py +5 -9
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_core_coverage.py +17 -99
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_gateway.py +8 -10
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_init_comprehensive.py +20 -23
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_init_simple.py +9 -13
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_main_class_extensive.py +13 -26
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/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.2}/.github/FUNDING.yml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.github/architect.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.github/ask.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.github/code.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.github/debug.chatmode.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.gitignore +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.idea/.gitignore +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.idea/misc.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.idea/modules.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.idea/python-selve-new.iml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/.idea/vcs.xml +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/CHANGELOG.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/LICENSE +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/README.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/coverage_xdist_example.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/debug_response.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/debug_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/direct_hardware_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/direct_hardware_test.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/generate_coverage.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/package.sh +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/python_selve_new.egg-info/SOURCES.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/python_selve_new.egg-info/dependency_links.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/python_selve_new.egg-info/top_level.txt +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/release.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/run_all_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/run_hardware_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/run_integration_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/run_mock_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/run_single_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/run_tests.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/command.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/device.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/event.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/firmware.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/group.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/iveo.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/param.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/senSim.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/sender.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/sensor.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/commands/service.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/device.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/gateway.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/group.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/iveo.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/senSim.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/sender.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/sensor.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/util/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/util/errors.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/selve/util/protocol.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/setup.cfg +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/setup_and_test.bat +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/README.md +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/test_selve_hardware.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/test_import.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/__init__.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/mock_utils.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_command_coverage.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_commands.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_device.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_device_classes_coverage.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_device_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_group.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_group_commands.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_param_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_edge_cases.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_selve_init_response_coverage.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_sender_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_sensim_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_sensor_commands_extended.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_service_commands.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/unit/test_util.py +0 -0
- {python_selve_new-2.5.1 → python_selve_new-2.5.2}/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.2
|
|
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.2
|
|
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,19 @@ except ImportError:
|
|
|
12
12
|
__version__ = "unknown"
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
-
import threading
|
|
16
15
|
import time
|
|
17
16
|
from collections import deque
|
|
18
17
|
from itertools import chain
|
|
19
18
|
from typing import Callable, Optional
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
from serial.tools import list_ports
|
|
23
|
-
|
|
20
|
+
try:
|
|
21
|
+
from serial.tools import list_ports as _serial_list_ports
|
|
22
|
+
def _comports():
|
|
23
|
+
return _serial_list_ports.comports()
|
|
24
|
+
except ImportError:
|
|
25
|
+
def _comports(): # type: ignore[misc]
|
|
26
|
+
return []
|
|
27
|
+
|
|
24
28
|
import untangle
|
|
25
29
|
|
|
26
30
|
from selve.commands import param, service
|
|
@@ -113,21 +117,21 @@ class Selve:
|
|
|
113
117
|
# Kept for backward compatibility in tests/mocks.
|
|
114
118
|
return True
|
|
115
119
|
|
|
116
|
-
def _build_transport(self, port: str):
|
|
120
|
+
async def _build_transport(self, port: str):
|
|
117
121
|
self._transport = SerialTransport(port=port, logger=self._LOGGER)
|
|
118
|
-
self._transport.ensure_open()
|
|
119
|
-
self._serial =
|
|
122
|
+
await self._transport.ensure_open()
|
|
123
|
+
self._serial = None
|
|
120
124
|
|
|
121
|
-
def _teardown_transport(self):
|
|
125
|
+
async def _teardown_transport(self):
|
|
122
126
|
if self._transport:
|
|
123
|
-
self._transport.shutdown()
|
|
127
|
+
await self._transport.shutdown()
|
|
124
128
|
self._transport = None
|
|
125
129
|
self._serial = None
|
|
126
130
|
|
|
127
131
|
async def _probe_port(self, port: str, fromConfigFlow: bool = False) -> bool:
|
|
128
132
|
"""Attempt to connect and verify a Selve gateway on the given port."""
|
|
129
133
|
try:
|
|
130
|
-
self._build_transport(port)
|
|
134
|
+
await self._build_transport(port)
|
|
131
135
|
ok = await self.pingGateway(fromConfigFlow=fromConfigFlow)
|
|
132
136
|
if ok:
|
|
133
137
|
try:
|
|
@@ -142,13 +146,12 @@ class Selve:
|
|
|
142
146
|
self._LOGGER.debug(f"Probe failed on {port}: {e}")
|
|
143
147
|
|
|
144
148
|
await self.stopWorker()
|
|
145
|
-
self._teardown_transport()
|
|
149
|
+
await self._teardown_transport()
|
|
146
150
|
return False
|
|
147
151
|
|
|
148
152
|
|
|
149
153
|
def list_ports(self):
|
|
150
|
-
|
|
151
|
-
return available_ports
|
|
154
|
+
return _comports()
|
|
152
155
|
|
|
153
156
|
async def check_port(self, port):
|
|
154
157
|
if port is not None:
|
|
@@ -172,22 +175,14 @@ class Selve:
|
|
|
172
175
|
await self.discover()
|
|
173
176
|
await self.startWorker()
|
|
174
177
|
return
|
|
175
|
-
except (
|
|
178
|
+
except (OSError, IOError) as e:
|
|
176
179
|
self._LOGGER.debug("Configured port not valid! " + str(e))
|
|
177
180
|
except Exception as e:
|
|
178
181
|
self._LOGGER.error("Unknown exception: " + str(e))
|
|
179
182
|
|
|
180
183
|
|
|
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()
|
|
184
|
+
loop = asyncio.get_running_loop()
|
|
185
|
+
available_ports = await loop.run_in_executor(None, _comports)
|
|
191
186
|
|
|
192
187
|
self._LOGGER.debug("available comports: " + str(available_ports))
|
|
193
188
|
|
|
@@ -216,31 +211,22 @@ class Selve:
|
|
|
216
211
|
await asyncio.sleep(5)
|
|
217
212
|
self._LOGGER.debug("(Selve Worker): " + "Recovering")
|
|
218
213
|
|
|
219
|
-
self._teardown_transport()
|
|
214
|
+
await self._teardown_transport()
|
|
220
215
|
|
|
221
216
|
if self._port is not None:
|
|
222
217
|
try:
|
|
223
218
|
if await self._probe_port(self._port, fromConfigFlow=False):
|
|
224
219
|
if self.rxQ is not None and self._transport is not None:
|
|
225
|
-
|
|
226
|
-
self._transport.start_reader(loop, self.rxQ)
|
|
220
|
+
await self._transport.start_reader(self.rxQ)
|
|
227
221
|
return
|
|
228
|
-
except (
|
|
222
|
+
except (OSError, IOError) as e:
|
|
229
223
|
self._LOGGER.debug("(Selve Worker): " + "Configured port not valid, maybe it has changed, trying other ports...")
|
|
230
224
|
except Exception as e:
|
|
231
225
|
self._LOGGER.error("(Selve Worker): " + "Unknown exception: " + str(e))
|
|
232
226
|
|
|
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
|
-
|
|
227
|
+
loop = asyncio.get_running_loop()
|
|
228
|
+
available_ports = await loop.run_in_executor(None, _comports)
|
|
229
|
+
|
|
244
230
|
self._LOGGER.debug("(Selve Worker): " + "available comports: " + str(available_ports))
|
|
245
231
|
|
|
246
232
|
if len(available_ports) == 0:
|
|
@@ -251,8 +237,7 @@ class Selve:
|
|
|
251
237
|
try:
|
|
252
238
|
if await self._probe_port(p.device, fromConfigFlow=False):
|
|
253
239
|
if self.rxQ is not None and self._transport is not None:
|
|
254
|
-
|
|
255
|
-
self._transport.start_reader(loop, self.rxQ)
|
|
240
|
+
await self._transport.start_reader(self.rxQ)
|
|
256
241
|
return
|
|
257
242
|
except Exception as e:
|
|
258
243
|
self._LOGGER.error("(Selve Worker): " + "Error at com port: " + str(e))
|
|
@@ -273,12 +258,11 @@ class Selve:
|
|
|
273
258
|
if self._event_queue is None or not isinstance(self._event_queue, asyncio.Queue):
|
|
274
259
|
self._event_queue = asyncio.Queue()
|
|
275
260
|
|
|
276
|
-
# Ensure transport and reader
|
|
277
|
-
loop = self.loop or asyncio.get_running_loop()
|
|
261
|
+
# Ensure transport and reader task are running
|
|
278
262
|
if self._transport is None and self._port is not None:
|
|
279
|
-
self._build_transport(self._port)
|
|
263
|
+
await self._build_transport(self._port)
|
|
280
264
|
if self._transport is not None:
|
|
281
|
-
self._transport.start_reader(
|
|
265
|
+
await self._transport.start_reader(self.rxQ)
|
|
282
266
|
|
|
283
267
|
if self._tx_task is None or self._tx_task.done():
|
|
284
268
|
self._tx_task = asyncio.create_task(self._tx_loop())
|
|
@@ -371,7 +355,7 @@ class Selve:
|
|
|
371
355
|
self._dispatch_task = None
|
|
372
356
|
self._pending_futures.clear()
|
|
373
357
|
if self._transport:
|
|
374
|
-
self._transport.stop_reader()
|
|
358
|
+
await self._transport.stop_reader()
|
|
375
359
|
if self._event_queue is not None:
|
|
376
360
|
while not self._event_queue.empty():
|
|
377
361
|
try:
|
|
@@ -387,7 +371,7 @@ class Selve:
|
|
|
387
371
|
self._LOGGER.debug("Preparing for termination")
|
|
388
372
|
await self.stopWorker()
|
|
389
373
|
# close the serial port, do the cleanup
|
|
390
|
-
self._teardown_transport()
|
|
374
|
+
await self._teardown_transport()
|
|
391
375
|
return True
|
|
392
376
|
|
|
393
377
|
|
|
@@ -428,20 +412,20 @@ class Selve:
|
|
|
428
412
|
if self._transport is None:
|
|
429
413
|
if self._port is None:
|
|
430
414
|
raise PortError("No serial port configured")
|
|
431
|
-
self._build_transport(self._port)
|
|
415
|
+
await self._build_transport(self._port)
|
|
432
416
|
|
|
433
417
|
await self._transport.write(commandstr)
|
|
434
418
|
# small pause to give the gateway time to answer
|
|
435
419
|
await asyncio.sleep(0.1)
|
|
436
420
|
|
|
437
|
-
except (
|
|
421
|
+
except (OSError, IOError) as se:
|
|
438
422
|
self._LOGGER.info('Serial error, trying to reconnect once... ' + str(se))
|
|
439
423
|
await self.recover()
|
|
440
424
|
|
|
441
425
|
try:
|
|
442
426
|
self._LOGGER.debug('Trying again...')
|
|
443
427
|
if self._transport is None and self._port is not None:
|
|
444
|
-
self._build_transport(self._port)
|
|
428
|
+
await self._build_transport(self._port)
|
|
445
429
|
await self._transport.write(commandstr)
|
|
446
430
|
await asyncio.sleep(0.1)
|
|
447
431
|
|
|
@@ -1188,7 +1172,8 @@ class Selve:
|
|
|
1188
1172
|
while await self.gatewayState() != ServiceState.READY:
|
|
1189
1173
|
if time.time() - start_time >= 30:
|
|
1190
1174
|
self._LOGGER.info("Error: Gateway could not be reset or loads too long")
|
|
1191
|
-
|
|
1175
|
+
break
|
|
1176
|
+
await asyncio.sleep(0.1)
|
|
1192
1177
|
self._LOGGER.info("Gateway reset")
|
|
1193
1178
|
|
|
1194
1179
|
async def factoryResetGateway(self):
|
|
@@ -1201,7 +1186,8 @@ class Selve:
|
|
|
1201
1186
|
while await self.gatewayState() != ServiceState.READY:
|
|
1202
1187
|
if time.time() - start_time >= 60:
|
|
1203
1188
|
self._LOGGER.info("Error: Gateway could not be reset or loads too long")
|
|
1204
|
-
|
|
1189
|
+
break
|
|
1190
|
+
await asyncio.sleep(0.1)
|
|
1205
1191
|
self._LOGGER.info("Gateway factory reset")
|
|
1206
1192
|
return response.executed
|
|
1207
1193
|
|
|
@@ -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.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 5, 2)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g24c865c13'
|
|
@@ -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
|
{python_selve_new-2.5.1 → python_selve_new-2.5.2}/tests/integration/test_device_integration.py
RENAMED
|
@@ -3,7 +3,6 @@ import logging
|
|
|
3
3
|
import sys
|
|
4
4
|
import asyncio
|
|
5
5
|
import os
|
|
6
|
-
import serial
|
|
7
6
|
from unittest.mock import MagicMock, patch, AsyncMock
|
|
8
7
|
|
|
9
8
|
# Import the Selve package
|
|
@@ -38,16 +37,17 @@ class TestDeviceIntegration(unittest.TestCase):
|
|
|
38
37
|
|
|
39
38
|
def setUp(self):
|
|
40
39
|
"""Set up mock serial port and other mocks."""
|
|
41
|
-
self.mock_serial_patcher = patch('selve.
|
|
40
|
+
self.mock_serial_patcher = patch('selve.util.serial_transport.SerialTransport')
|
|
42
41
|
self.mock_serial = self.mock_serial_patcher.start()
|
|
43
|
-
|
|
44
|
-
# Configure mock
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
|
|
43
|
+
# Configure mock transport
|
|
44
|
+
mock_transport_instance = self.mock_serial.return_value
|
|
45
|
+
mock_transport_instance.is_open = True
|
|
46
|
+
mock_transport_instance.ensure_open = AsyncMock()
|
|
47
|
+
mock_transport_instance.start_reader = AsyncMock()
|
|
48
|
+
mock_transport_instance.stop_reader = AsyncMock()
|
|
49
|
+
mock_transport_instance.write = AsyncMock()
|
|
50
|
+
mock_transport_instance.shutdown = AsyncMock()
|
|
51
51
|
|
|
52
52
|
# Create the Selve instance
|
|
53
53
|
self.selve = Selve(port="COM3", discover=False, develop=True,
|
|
@@ -241,7 +241,7 @@ class TestDeviceIntegration(unittest.TestCase):
|
|
|
241
241
|
def test_connection_error_handling(self):
|
|
242
242
|
"""Test handling of connection errors in device commands."""
|
|
243
243
|
# Make executeCommandSyncWithResponse raise an exception
|
|
244
|
-
self.selve.executeCommandSyncWithResponse.side_effect =
|
|
244
|
+
self.selve.executeCommandSyncWithResponse.side_effect = OSError("Connection failed")
|
|
245
245
|
|
|
246
246
|
# Setup the test
|
|
247
247
|
async def test_async():
|
|
@@ -250,7 +250,7 @@ class TestDeviceIntegration(unittest.TestCase):
|
|
|
250
250
|
result = await self.selve.moveDeviceUp(self.device)
|
|
251
251
|
# The method should have caught the exception and returned False
|
|
252
252
|
self.assertFalse(result, "Method should return False on serial exception")
|
|
253
|
-
except
|
|
253
|
+
except OSError:
|
|
254
254
|
# If the exception wasn't caught, we need to patch the method
|
|
255
255
|
from unittest.mock import patch
|
|
256
256
|
|
|
@@ -42,9 +42,6 @@ class TestSelveGatewayIntegration:
|
|
|
42
42
|
|
|
43
43
|
# Verify port was set correctly
|
|
44
44
|
assert selve._port == "COM3"
|
|
45
|
-
|
|
46
|
-
# Verify serial was initialized
|
|
47
|
-
assert selve._serial is not None
|
|
48
45
|
|
|
49
46
|
@pytest.mark.asyncio
|
|
50
47
|
async def test_gateway_setup_with_invalid_port(self, mock_serial, logger):
|
|
@@ -85,14 +82,8 @@ class TestSelveGatewayIntegration:
|
|
|
85
82
|
# Directly test the recover method with mocked components
|
|
86
83
|
with patch.object(mock_selve_instance, '_LOGGER') as mock_logger:
|
|
87
84
|
with patch('asyncio.sleep', return_value=None): # Skip the sleep delay
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
mock_serial_instance = MagicMock()
|
|
91
|
-
mock_serial_instance.is_open = True
|
|
92
|
-
mock_serial_class.return_value = mock_serial_instance
|
|
93
|
-
|
|
94
|
-
# Mock _probe_port to return True immediately
|
|
95
|
-
mock_selve_instance._probe_port = AsyncMock(return_value=True)
|
|
85
|
+
# Mock _probe_port to return True immediately
|
|
86
|
+
with patch.object(mock_selve_instance, '_probe_port', AsyncMock(return_value=True)):
|
|
96
87
|
mock_selve_instance._port = "COM3"
|
|
97
88
|
|
|
98
89
|
# Call recover directly
|