PyPlumIO 0.5.37__tar.gz → 0.5.39__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.
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.pre-commit-config.yaml +2 -2
- {pyplumio-0.5.37 → pyplumio-0.5.39}/PKG-INFO +10 -9
- {pyplumio-0.5.37 → pyplumio-0.5.39}/PyPlumIO.egg-info/PKG-INFO +10 -9
- {pyplumio-0.5.37 → pyplumio-0.5.39}/PyPlumIO.egg-info/requires.txt +7 -7
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/schedules.rst +16 -12
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/_version.py +9 -4
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/const.py +8 -4
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/devices/ecomax.py +20 -22
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/factory.py +1 -1
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/parameter.py +3 -3
- pyplumio-0.5.39/pyplumio/helpers/schedule.py +176 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/uid.py +2 -2
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/alerts.py +1 -1
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/outputs.py +1 -2
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/product_info.py +2 -2
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyproject.toml +7 -8
- {pyplumio-0.5.37 → pyplumio-0.5.39}/requirements.txt +1 -1
- pyplumio-0.5.39/requirements_test.txt +11 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_parameter.py +2 -2
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_schedule.py +50 -26
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_uid.py +2 -2
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_devices.py +7 -4
- pyplumio-0.5.37/pyplumio/helpers/schedule.py +0 -178
- pyplumio-0.5.37/requirements_test.txt +0 -11
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.gitattributes +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.gitignore +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/.vscode/settings.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/LICENSE +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/MANIFEST.in +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/README.md +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/Makefile +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/make.bat +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/conf.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/index.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/images/ecomax.png +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/images/rs485.png +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/devices/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/data_types.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/requirements_docs.txt +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/setup.cfg +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/conftest.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/ruff.toml +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_connection.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_filters.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_init.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_main.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_stream.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/test_utils.py +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.37 → pyplumio-0.5.39}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -2,7 +2,7 @@
|
|
2
2
|
# See https://pre-commit.com/hooks.html for more hooks
|
3
3
|
repos:
|
4
4
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
5
|
-
rev: v0.
|
5
|
+
rev: v0.11.2
|
6
6
|
hooks:
|
7
7
|
- id: ruff
|
8
8
|
args:
|
@@ -12,6 +12,6 @@ repos:
|
|
12
12
|
hooks:
|
13
13
|
- id: codespell
|
14
14
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
15
|
-
rev: v1.
|
15
|
+
rev: v1.15.0
|
16
16
|
hooks:
|
17
17
|
- id: mypy
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.39
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -28,22 +28,23 @@ Requires-Dist: pyserial-asyncio==0.6
|
|
28
28
|
Requires-Dist: typing-extensions==4.12.2
|
29
29
|
Provides-Extra: test
|
30
30
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
-
Requires-Dist: coverage==7.
|
32
|
-
Requires-Dist: mypy==1.
|
31
|
+
Requires-Dist: coverage==7.8.0; extra == "test"
|
32
|
+
Requires-Dist: mypy==1.15.0; extra == "test"
|
33
33
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
34
|
-
Requires-Dist: pytest==8.3.
|
34
|
+
Requires-Dist: pytest==8.3.5; extra == "test"
|
35
35
|
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
-
Requires-Dist: ruff==0.
|
37
|
-
Requires-Dist: tox==4.24.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
36
|
+
Requires-Dist: ruff==0.11.2; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.2; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250304; extra == "test"
|
39
39
|
Provides-Extra: docs
|
40
40
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
41
41
|
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
42
42
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
43
43
|
Provides-Extra: dev
|
44
44
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
45
|
-
Requires-Dist: pre-commit==4.
|
45
|
+
Requires-Dist: pre-commit==4.2.0; extra == "dev"
|
46
46
|
Requires-Dist: tomli==2.2.1; extra == "dev"
|
47
|
+
Dynamic: license-file
|
47
48
|
|
48
49
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
49
50
|
[](https://badge.fury.io/py/PyPlumIO)
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.39
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -28,22 +28,23 @@ Requires-Dist: pyserial-asyncio==0.6
|
|
28
28
|
Requires-Dist: typing-extensions==4.12.2
|
29
29
|
Provides-Extra: test
|
30
30
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
-
Requires-Dist: coverage==7.
|
32
|
-
Requires-Dist: mypy==1.
|
31
|
+
Requires-Dist: coverage==7.8.0; extra == "test"
|
32
|
+
Requires-Dist: mypy==1.15.0; extra == "test"
|
33
33
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
34
|
-
Requires-Dist: pytest==8.3.
|
34
|
+
Requires-Dist: pytest==8.3.5; extra == "test"
|
35
35
|
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
-
Requires-Dist: ruff==0.
|
37
|
-
Requires-Dist: tox==4.24.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
36
|
+
Requires-Dist: ruff==0.11.2; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.2; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250304; extra == "test"
|
39
39
|
Provides-Extra: docs
|
40
40
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
41
41
|
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
42
42
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
43
43
|
Provides-Extra: dev
|
44
44
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
45
|
-
Requires-Dist: pre-commit==4.
|
45
|
+
Requires-Dist: pre-commit==4.2.0; extra == "dev"
|
46
46
|
Requires-Dist: tomli==2.2.1; extra == "dev"
|
47
|
+
Dynamic: license-file
|
47
48
|
|
48
49
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
49
50
|
[](https://badge.fury.io/py/PyPlumIO)
|
@@ -4,7 +4,7 @@ typing-extensions==4.12.2
|
|
4
4
|
|
5
5
|
[dev]
|
6
6
|
pyplumio[docs,test]
|
7
|
-
pre-commit==4.
|
7
|
+
pre-commit==4.2.0
|
8
8
|
tomli==2.2.1
|
9
9
|
|
10
10
|
[docs]
|
@@ -14,11 +14,11 @@ readthedocs-sphinx-search==0.3.2
|
|
14
14
|
|
15
15
|
[test]
|
16
16
|
codespell==2.4.1
|
17
|
-
coverage==7.
|
18
|
-
mypy==1.
|
17
|
+
coverage==7.8.0
|
18
|
+
mypy==1.15.0
|
19
19
|
pyserial-asyncio-fast==0.14
|
20
|
-
pytest==8.3.
|
20
|
+
pytest==8.3.5
|
21
21
|
pytest-asyncio==0.25.3
|
22
|
-
ruff==0.
|
23
|
-
tox==4.24.
|
24
|
-
types-pyserial==3.5.0.
|
22
|
+
ruff==0.11.2
|
23
|
+
tox==4.24.2
|
24
|
+
types-pyserial==3.5.0.20250304
|
@@ -42,9 +42,11 @@ temperature by 10 degrees Celsius.
|
|
42
42
|
Setting Schedule
|
43
43
|
----------------
|
44
44
|
|
45
|
-
To set the schedule, you can
|
46
|
-
``
|
47
|
-
|
45
|
+
To set the schedule, you can either directly set the state via key or
|
46
|
+
by using ``set_state(state)``, ``set_on()`` or ``set_off()``.
|
47
|
+
|
48
|
+
After updating the state you must call ``commit()`` method to save
|
49
|
+
changes on the device.
|
48
50
|
|
49
51
|
This example sets nighttime mode for Monday from 00:00 to 07:00 and
|
50
52
|
switches back to daytime mode from 07:00 to 00:00.
|
@@ -57,14 +59,15 @@ switches back to daytime mode from 07:00 to 00:00.
|
|
57
59
|
heating_schedule.monday.set_on(start="07:00", end="00:00")
|
58
60
|
await heating_schedule.commit()
|
59
61
|
|
60
|
-
For clarity sake, you might want to use ``
|
61
|
-
``
|
62
|
+
For clarity sake, you might want to use ``STATE_OFF`` and
|
63
|
+
``STATE_ON`` constants from ``pyplumio.helpers.schedule`` module.
|
62
64
|
|
63
65
|
.. code-block:: python
|
64
66
|
|
65
|
-
from pyplumio.helpers.schedule import
|
67
|
+
from pyplumio.helpers.schedule import STATE_OFF
|
66
68
|
|
67
|
-
heating_schedule.monday
|
69
|
+
heating_schedule.monday["18:00"] = STATE_OFF
|
70
|
+
heating_schedule.monday.set_state(STATE_OFF, "00:00", "07:00")
|
68
71
|
|
69
72
|
You may also omit one of the boundaries.
|
70
73
|
The other boundary is then set to the end or start of the day.
|
@@ -107,7 +110,7 @@ Schedule Examples
|
|
107
110
|
.. code-block:: python
|
108
111
|
|
109
112
|
import pyplumio
|
110
|
-
from pyplumio.helpers.schedule import
|
113
|
+
from pyplumio.helpers.schedule import STATE_ON, STATE_OFF
|
111
114
|
|
112
115
|
|
113
116
|
async def main():
|
@@ -124,12 +127,13 @@ Schedule Examples
|
|
124
127
|
await ecomax.set("schedule_heating_parameter", 10)
|
125
128
|
|
126
129
|
for weekday in heating_schedule:
|
127
|
-
weekday.set_state(
|
128
|
-
weekday.set_state(
|
129
|
-
weekday.set_state(
|
130
|
+
weekday.set_state(STATE_ON, "00:00", "00:30")
|
131
|
+
weekday.set_state(STATE_OFF, "00:30", "09:00")
|
132
|
+
weekday.set_state(STATE_ON, "09:00", "00:00")
|
133
|
+
weekday["19:00"] = STATE_OFF
|
130
134
|
|
131
135
|
# There will be no nighttime mode on sunday.
|
132
|
-
heating_schedule.sunday.set_state(
|
136
|
+
heating_schedule.sunday.set_state(STATE_ON)
|
133
137
|
|
134
138
|
await heating_schedule.commit()
|
135
139
|
|
@@ -1,8 +1,13 @@
|
|
1
|
-
# file generated by
|
1
|
+
# file generated by setuptools-scm
|
2
2
|
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
3
6
|
TYPE_CHECKING = False
|
4
7
|
if TYPE_CHECKING:
|
5
|
-
from typing import Tuple
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
12
|
else:
|
8
13
|
VERSION_TUPLE = object
|
@@ -12,5 +17,5 @@ __version__: str
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
13
18
|
version_tuple: VERSION_TUPLE
|
14
19
|
|
15
|
-
__version__ = version = '0.5.
|
16
|
-
__version_tuple__ = version_tuple = (0, 5,
|
20
|
+
__version__ = version = '0.5.39'
|
21
|
+
__version_tuple__ = version_tuple = (0, 5, 39)
|
@@ -3,11 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from enum import Enum, IntEnum, unique
|
6
|
-
from typing import Any, Final
|
6
|
+
from typing import Any, Final, Literal
|
7
7
|
|
8
|
-
|
9
|
-
STATE_ON: Final = "on"
|
10
|
-
STATE_OFF: Final = "off"
|
8
|
+
from typing_extensions import TypeAlias
|
11
9
|
|
12
10
|
# General attributes.
|
13
11
|
ATTR_CONNECTED: Final = "connected"
|
@@ -221,3 +219,9 @@ class UnitOfMeasurement(Enum):
|
|
221
219
|
|
222
220
|
|
223
221
|
PERCENTAGE: Final = "%"
|
222
|
+
|
223
|
+
STATE_ON: Final = "on"
|
224
|
+
STATE_OFF: Final = "off"
|
225
|
+
|
226
|
+
|
227
|
+
State: TypeAlias = Literal["on", "off"]
|
@@ -21,7 +21,7 @@ from pyplumio.devices.mixer import Mixer
|
|
21
21
|
from pyplumio.devices.thermostat import Thermostat
|
22
22
|
from pyplumio.filters import on_change
|
23
23
|
from pyplumio.frames import DataFrameDescription, Frame, Request
|
24
|
-
from pyplumio.helpers.parameter import ParameterValues
|
24
|
+
from pyplumio.helpers.parameter import STATE_OFF, STATE_ON, ParameterValues, State
|
25
25
|
from pyplumio.helpers.schedule import Schedule, ScheduleDay
|
26
26
|
from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
|
27
27
|
from pyplumio.structures.ecomax_parameters import (
|
@@ -99,8 +99,6 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
|
99
99
|
|
100
100
|
_LOGGER = logging.getLogger(__name__)
|
101
101
|
|
102
|
-
ecomax_control_error = "ecoMAX control is not available. Please try again later."
|
103
|
-
|
104
102
|
|
105
103
|
class EcoMAX(PhysicalDevice):
|
106
104
|
"""Represents an ecoMAX controller."""
|
@@ -282,13 +280,13 @@ class EcoMAX(PhysicalDevice):
|
|
282
280
|
SCHEDULES[index]: Schedule(
|
283
281
|
name=SCHEDULES[index],
|
284
282
|
device=self,
|
285
|
-
monday=ScheduleDay(schedule[1]),
|
286
|
-
tuesday=ScheduleDay(schedule[2]),
|
287
|
-
wednesday=ScheduleDay(schedule[3]),
|
288
|
-
thursday=ScheduleDay(schedule[4]),
|
289
|
-
friday=ScheduleDay(schedule[5]),
|
290
|
-
saturday=ScheduleDay(schedule[6]),
|
291
|
-
sunday=ScheduleDay(schedule[0]),
|
283
|
+
monday=ScheduleDay.from_iterable(schedule[1]),
|
284
|
+
tuesday=ScheduleDay.from_iterable(schedule[2]),
|
285
|
+
wednesday=ScheduleDay.from_iterable(schedule[3]),
|
286
|
+
thursday=ScheduleDay.from_iterable(schedule[4]),
|
287
|
+
friday=ScheduleDay.from_iterable(schedule[5]),
|
288
|
+
saturday=ScheduleDay.from_iterable(schedule[6]),
|
289
|
+
sunday=ScheduleDay.from_iterable(schedule[0]),
|
292
290
|
)
|
293
291
|
for index, schedule in schedules
|
294
292
|
}
|
@@ -398,23 +396,23 @@ class EcoMAX(PhysicalDevice):
|
|
398
396
|
|
399
397
|
return False
|
400
398
|
|
401
|
-
async def
|
402
|
-
"""
|
399
|
+
async def _set_ecomax_state(self, state: State) -> bool:
|
400
|
+
"""Try to set the ecoMAX control state."""
|
403
401
|
try:
|
404
|
-
|
405
|
-
return await
|
402
|
+
switch: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
|
403
|
+
return await switch.set(state)
|
406
404
|
except KeyError:
|
407
|
-
_LOGGER.error(
|
408
|
-
|
405
|
+
_LOGGER.error("ecoMAX control is not available. Please try again later.")
|
406
|
+
|
407
|
+
return False
|
408
|
+
|
409
|
+
async def turn_on(self) -> bool:
|
410
|
+
"""Turn on the ecoMAX controller."""
|
411
|
+
return await self._set_ecomax_state(STATE_ON)
|
409
412
|
|
410
413
|
async def turn_off(self) -> bool:
|
411
414
|
"""Turn off the ecoMAX controller."""
|
412
|
-
|
413
|
-
ecomax_control: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
|
414
|
-
return await ecomax_control.turn_off()
|
415
|
-
except KeyError:
|
416
|
-
_LOGGER.error(ecomax_control_error)
|
417
|
-
return False
|
415
|
+
return await self._set_ecomax_state(STATE_OFF)
|
418
416
|
|
419
417
|
def turn_on_nowait(self) -> None:
|
420
418
|
"""Turn on the ecoMAX controller without waiting."""
|
@@ -19,7 +19,7 @@ async def _import_module(name: str) -> ModuleType:
|
|
19
19
|
return await loop.run_in_executor(None, importlib.import_module, f"pyplumio.{name}")
|
20
20
|
|
21
21
|
|
22
|
-
async def create_instance(class_path: str, cls: type[T], **kwargs: Any) -> T:
|
22
|
+
async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
|
23
23
|
"""Return a class instance from the class path."""
|
24
24
|
module_name, class_name = class_path.rsplit(".", 1)
|
25
25
|
try:
|
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
|
11
11
|
from dataslots import dataslots
|
12
12
|
from typing_extensions import TypeAlias
|
13
13
|
|
14
|
-
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, UnitOfMeasurement
|
14
|
+
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
|
15
15
|
from pyplumio.frames import Request
|
16
16
|
|
17
17
|
if TYPE_CHECKING:
|
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
21
21
|
|
22
|
+
|
22
23
|
NumericType: TypeAlias = Union[int, float]
|
23
|
-
State: TypeAlias = Literal["on", "off"]
|
24
24
|
ParameterT = TypeVar("ParameterT", bound="Parameter")
|
25
25
|
|
26
26
|
|
@@ -108,7 +108,7 @@ class Parameter(ABC):
|
|
108
108
|
"""Return a serializable string representation."""
|
109
109
|
return (
|
110
110
|
f"{self.__class__.__name__}("
|
111
|
-
f"device={self.device
|
111
|
+
f"device={self.device}, "
|
112
112
|
f"description={self.description}, "
|
113
113
|
f"values={self.values}, "
|
114
114
|
f"index={self._index})"
|
@@ -0,0 +1,176 @@
|
|
1
|
+
"""Contains a schedule helper classes."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections.abc import Iterable, Iterator, MutableMapping
|
6
|
+
from dataclasses import dataclass
|
7
|
+
import datetime as dt
|
8
|
+
from functools import lru_cache
|
9
|
+
from typing import Annotated, Final, get_args
|
10
|
+
|
11
|
+
from pyplumio.const import STATE_OFF, STATE_ON, FrameType, State
|
12
|
+
from pyplumio.devices import PhysicalDevice
|
13
|
+
from pyplumio.frames import Request
|
14
|
+
from pyplumio.structures.schedules import collect_schedule_data
|
15
|
+
|
16
|
+
TIME_FORMAT: Final = "%H:%M"
|
17
|
+
|
18
|
+
MIDNIGHT: Final = "00:00"
|
19
|
+
MIDNIGHT_DT = dt.datetime.strptime(MIDNIGHT, TIME_FORMAT)
|
20
|
+
|
21
|
+
STEP = dt.timedelta(minutes=30)
|
22
|
+
|
23
|
+
Time = Annotated[str, "Time string in %H:%M format"]
|
24
|
+
|
25
|
+
|
26
|
+
def _get_time(
|
27
|
+
index: int, start: dt.datetime = MIDNIGHT_DT, step: dt.timedelta = STEP
|
28
|
+
) -> Time:
|
29
|
+
"""Return time for a specific index."""
|
30
|
+
time_dt = start + (step * index)
|
31
|
+
return time_dt.strftime(TIME_FORMAT)
|
32
|
+
|
33
|
+
|
34
|
+
@lru_cache(maxsize=10)
|
35
|
+
def _get_time_range(start: Time, end: Time, step: dt.timedelta = STEP) -> list[Time]:
|
36
|
+
"""Get a time range.
|
37
|
+
|
38
|
+
Start and end boundaries should be specified in %H:%M format.
|
39
|
+
Both are inclusive.
|
40
|
+
"""
|
41
|
+
start_dt = dt.datetime.strptime(start, TIME_FORMAT)
|
42
|
+
end_dt = dt.datetime.strptime(end, TIME_FORMAT)
|
43
|
+
|
44
|
+
if end_dt == MIDNIGHT_DT:
|
45
|
+
# Upper boundary of the interval is midnight.
|
46
|
+
end_dt += dt.timedelta(hours=24) - step
|
47
|
+
|
48
|
+
if end_dt <= start_dt:
|
49
|
+
raise ValueError(
|
50
|
+
f"Invalid time range: start time ({start}) must be earlier "
|
51
|
+
f"than end time ({end})."
|
52
|
+
)
|
53
|
+
|
54
|
+
seconds = (end_dt - start_dt).total_seconds()
|
55
|
+
steps = seconds // step.total_seconds() + 1
|
56
|
+
|
57
|
+
return [_get_time(index, start=start_dt, step=step) for index in range(int(steps))]
|
58
|
+
|
59
|
+
|
60
|
+
class ScheduleDay(MutableMapping):
|
61
|
+
"""Represents a single day of schedule."""
|
62
|
+
|
63
|
+
__slots__ = ("_schedule",)
|
64
|
+
|
65
|
+
_schedule: dict[Time, bool]
|
66
|
+
|
67
|
+
def __init__(self, schedule: dict[Time, bool]) -> None:
|
68
|
+
"""Initialize a new schedule day."""
|
69
|
+
self._schedule = schedule
|
70
|
+
|
71
|
+
def __repr__(self) -> str:
|
72
|
+
"""Return serializable representation of the class."""
|
73
|
+
return f"ScheduleDay({self._schedule})"
|
74
|
+
|
75
|
+
def __len__(self) -> int:
|
76
|
+
"""Return a schedule length."""
|
77
|
+
return self._schedule.__len__()
|
78
|
+
|
79
|
+
def __iter__(self) -> Iterator[Time]:
|
80
|
+
"""Return an iterator."""
|
81
|
+
return self._schedule.__iter__()
|
82
|
+
|
83
|
+
def __getitem__(self, time: Time) -> State:
|
84
|
+
"""Return a schedule item."""
|
85
|
+
state = self._schedule.__getitem__(time)
|
86
|
+
return STATE_ON if state else STATE_OFF
|
87
|
+
|
88
|
+
def __delitem__(self, time: Time) -> None:
|
89
|
+
"""Delete a schedule item."""
|
90
|
+
self._schedule.__delitem__(time)
|
91
|
+
|
92
|
+
def __setitem__(self, time: Time, state: State | bool) -> None:
|
93
|
+
"""Set a schedule item."""
|
94
|
+
if state in get_args(State):
|
95
|
+
state = True if state == STATE_ON else False
|
96
|
+
if isinstance(state, bool):
|
97
|
+
self._schedule.__setitem__(time, state)
|
98
|
+
else:
|
99
|
+
raise TypeError(
|
100
|
+
f"Expected boolean value or one of: {', '.join(get_args(State))}."
|
101
|
+
)
|
102
|
+
|
103
|
+
def set_state(
|
104
|
+
self, state: State | bool, start: Time = MIDNIGHT, end: Time = MIDNIGHT
|
105
|
+
) -> None:
|
106
|
+
"""Set a schedule interval state."""
|
107
|
+
for time in _get_time_range(start, end):
|
108
|
+
self.__setitem__(time, state)
|
109
|
+
|
110
|
+
def set_on(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
|
111
|
+
"""Set a schedule interval state to 'on'."""
|
112
|
+
self.set_state(STATE_ON, start, end)
|
113
|
+
|
114
|
+
def set_off(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
|
115
|
+
"""Set a schedule interval state to 'off'."""
|
116
|
+
self.set_state(STATE_OFF, start, end)
|
117
|
+
|
118
|
+
@property
|
119
|
+
def schedule(self) -> dict[Time, bool]:
|
120
|
+
"""Return the schedule."""
|
121
|
+
return self._schedule
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
def from_iterable(cls: type[ScheduleDay], intervals: Iterable[bool]) -> ScheduleDay:
|
125
|
+
"""Make schedule day from iterable."""
|
126
|
+
return cls({_get_time(index): state for index, state in enumerate(intervals)})
|
127
|
+
|
128
|
+
|
129
|
+
@dataclass
|
130
|
+
class Schedule(Iterable):
|
131
|
+
"""Represents a weekly schedule."""
|
132
|
+
|
133
|
+
__slots__ = (
|
134
|
+
"name",
|
135
|
+
"device",
|
136
|
+
"sunday",
|
137
|
+
"monday",
|
138
|
+
"tuesday",
|
139
|
+
"wednesday",
|
140
|
+
"thursday",
|
141
|
+
"friday",
|
142
|
+
"saturday",
|
143
|
+
)
|
144
|
+
|
145
|
+
name: str
|
146
|
+
device: PhysicalDevice
|
147
|
+
|
148
|
+
sunday: ScheduleDay
|
149
|
+
monday: ScheduleDay
|
150
|
+
tuesday: ScheduleDay
|
151
|
+
wednesday: ScheduleDay
|
152
|
+
thursday: ScheduleDay
|
153
|
+
friday: ScheduleDay
|
154
|
+
saturday: ScheduleDay
|
155
|
+
|
156
|
+
def __iter__(self) -> Iterator[ScheduleDay]:
|
157
|
+
"""Return list of days."""
|
158
|
+
return (
|
159
|
+
self.sunday,
|
160
|
+
self.monday,
|
161
|
+
self.tuesday,
|
162
|
+
self.wednesday,
|
163
|
+
self.thursday,
|
164
|
+
self.friday,
|
165
|
+
self.saturday,
|
166
|
+
).__iter__()
|
167
|
+
|
168
|
+
async def commit(self) -> None:
|
169
|
+
"""Commit a weekly schedule to the device."""
|
170
|
+
await self.device.queue.put(
|
171
|
+
await Request.create(
|
172
|
+
FrameType.REQUEST_SET_SCHEDULE,
|
173
|
+
recipient=self.device.address,
|
174
|
+
data=collect_schedule_data(self.name, self.device),
|
175
|
+
)
|
176
|
+
)
|
@@ -10,8 +10,8 @@ POLYNOMIAL: Final = 0xA001
|
|
10
10
|
BASE5_KEY: Final = "0123456789ABCDEFGHIJKLMNZPQRSTUV"
|
11
11
|
|
12
12
|
|
13
|
-
def
|
14
|
-
"""
|
13
|
+
def unpack_uid(buffer: bytes) -> str:
|
14
|
+
"""Unpack UID from bytes."""
|
15
15
|
return _base5(buffer + _crc16(buffer))
|
16
16
|
|
17
17
|
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
import math
|
6
5
|
from typing import Any, Final
|
7
6
|
|
8
7
|
from pyplumio.helpers.data_types import UnsignedInt
|
@@ -60,7 +59,7 @@ class OutputsStructure(StructureDecoder):
|
|
60
59
|
ensure_dict(
|
61
60
|
data,
|
62
61
|
{
|
63
|
-
output: bool(outputs.value &
|
62
|
+
output: bool(outputs.value & 2**index)
|
64
63
|
for index, output in enumerate(OUTPUTS)
|
65
64
|
},
|
66
65
|
),
|
@@ -10,7 +10,7 @@ from typing import Any, Final
|
|
10
10
|
|
11
11
|
from pyplumio.const import ProductType
|
12
12
|
from pyplumio.helpers.data_types import UnsignedShort, VarBytes, VarString
|
13
|
-
from pyplumio.helpers.uid import
|
13
|
+
from pyplumio.helpers.uid import unpack_uid
|
14
14
|
from pyplumio.structures import StructureDecoder
|
15
15
|
from pyplumio.utils import ensure_dict
|
16
16
|
|
@@ -69,7 +69,7 @@ class ProductInfoStructure(StructureDecoder):
|
|
69
69
|
ATTR_PRODUCT: ProductInfo(
|
70
70
|
type=ProductType(product_type),
|
71
71
|
id=product_id,
|
72
|
-
uid=
|
72
|
+
uid=unpack_uid(uid.value),
|
73
73
|
logo=logo.value,
|
74
74
|
image=image.value,
|
75
75
|
model=format_model_name(model_name.value),
|
@@ -38,14 +38,14 @@ dynamic = ["version"]
|
|
38
38
|
[project.optional-dependencies]
|
39
39
|
test = [
|
40
40
|
"codespell==2.4.1",
|
41
|
-
"coverage==7.
|
42
|
-
"mypy==1.
|
41
|
+
"coverage==7.8.0",
|
42
|
+
"mypy==1.15.0",
|
43
43
|
"pyserial-asyncio-fast==0.14",
|
44
|
-
"pytest==8.3.
|
44
|
+
"pytest==8.3.5",
|
45
45
|
"pytest-asyncio==0.25.3",
|
46
|
-
"ruff==0.
|
47
|
-
"tox==4.24.
|
48
|
-
"types-pyserial==3.5.0.
|
46
|
+
"ruff==0.11.2",
|
47
|
+
"tox==4.24.2",
|
48
|
+
"types-pyserial==3.5.0.20250304"
|
49
49
|
]
|
50
50
|
docs = [
|
51
51
|
"sphinx==8.1.3",
|
@@ -54,7 +54,7 @@ docs = [
|
|
54
54
|
]
|
55
55
|
dev = [
|
56
56
|
"pyplumio[test,docs]",
|
57
|
-
"pre-commit==4.
|
57
|
+
"pre-commit==4.2.0",
|
58
58
|
"tomli==2.2.1"
|
59
59
|
]
|
60
60
|
|
@@ -179,7 +179,6 @@ select = [
|
|
179
179
|
"S317", # suspicious-xml-sax-usage
|
180
180
|
"S318", # suspicious-xml-mini-dom-usage
|
181
181
|
"S319", # suspicious-xml-pull-dom-usage
|
182
|
-
"S320", # suspicious-xmle-tree-usage
|
183
182
|
"S601", # paramiko-call
|
184
183
|
"S602", # subprocess-popen-with-shell-equals-true
|
185
184
|
"S604", # call-with-shell-equals-true
|
@@ -284,7 +284,7 @@ def test_switch_int(switch: Switch) -> None:
|
|
284
284
|
def test_number_repr(number: Number) -> None:
|
285
285
|
"""Test a number representation."""
|
286
286
|
assert repr(number) == (
|
287
|
-
"Number(device=
|
287
|
+
f"Number(device={number.device}, "
|
288
288
|
"description=NumberDescription(name='test_number', optimistic=False, "
|
289
289
|
"unit_of_measurement=<UnitOfMeasurement.CELSIUS: '°C'>), "
|
290
290
|
"values=ParameterValues(value=1, min_value=0, max_value=5), "
|
@@ -295,7 +295,7 @@ def test_number_repr(number: Number) -> None:
|
|
295
295
|
def test_switch_repr(switch: Switch) -> None:
|
296
296
|
"""Test a number representation."""
|
297
297
|
assert repr(switch) == (
|
298
|
-
"Switch(device=
|
298
|
+
f"Switch(device={switch.device}, "
|
299
299
|
"description=SwitchDescription(name='test_switch', optimistic=False), "
|
300
300
|
"values=ParameterValues(value=0, min_value=0, max_value=1), "
|
301
301
|
"index=0)"
|