PyPlumIO 0.5.55__tar.gz → 0.5.56__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.55 → pyplumio-0.5.56}/PKG-INFO +1 -1
- {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/PKG-INFO +1 -1
- {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/SOURCES.txt +1 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/index.rst +1 -0
- pyplumio-0.5.56/docs/source/statistics.rst +40 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/_version.py +3 -3
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/protocol.py +113 -3
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_protocol.py +69 -7
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.gitattributes +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/codeql.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.gitignore +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.pre-commit-config.yaml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.qlty/qlty.toml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/.vscode/settings.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/LICENSE +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/MANIFEST.in +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/requires.txt +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/README.md +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/Makefile +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/make.bat +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/conf.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/images/ecomax.png +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/images/rs485.png +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/const.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/data_types.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/ecomax.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/async_cache.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/custom/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/ecomax.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/mixer.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/thermostat.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/pyproject.toml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/requirements.txt +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/requirements_docs.txt +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/requirements_test.txt +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/setup.cfg +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/conftest.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_ecomax.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_ecoster.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_init.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_mixer.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_thermostat.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_async_cache.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/custom/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/custom/test_ecomax_860d3_hb.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/custom/test_init.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_ecomax.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_init.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_mixers.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_thermostats.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/ruff.toml +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/__init__.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_alerts.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_boiler_load.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_boiler_power.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_ecomax_parameters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_fan_power.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_frame_versions.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_fuel_consumption.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_fuel_level.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_lambda_sensor.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_mixer_parameters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_product_info.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_schedules.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_connection.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_data_types.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_filters.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_init.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_main.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_stream.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_utils.py +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/parameters/ecomax_860d3_hb.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
Statistics
|
2
|
+
==========
|
3
|
+
|
4
|
+
About Statistics
|
5
|
+
----------------
|
6
|
+
|
7
|
+
Since PyPlumIO v0.5.56, you can access statistics via following property.
|
8
|
+
|
9
|
+
.. autoattribute:: pyplumio.protocol.AsyncProtocol.statistics
|
10
|
+
|
11
|
+
Statistics contain transfer data consisting of number of received/sent frames and bytes
|
12
|
+
as well as datetime of when connection was established, when connection was lost and
|
13
|
+
number of connection loss event.
|
14
|
+
|
15
|
+
.. autoclass:: pyplumio.protocol.Statistics
|
16
|
+
|
17
|
+
The `devices` property of statistics class of also contains a list of
|
18
|
+
device statistics objects.
|
19
|
+
|
20
|
+
.. autoclass:: pyplumio.protocol.DeviceStatistics
|
21
|
+
|
22
|
+
Statistics Examples
|
23
|
+
-------------------
|
24
|
+
|
25
|
+
You can easily access statistic object via proxy call through Connection object
|
26
|
+
as in example below.
|
27
|
+
|
28
|
+
.. code-block:: python
|
29
|
+
|
30
|
+
import asyncio
|
31
|
+
|
32
|
+
import pyplumio
|
33
|
+
|
34
|
+
async def main():
|
35
|
+
"""Read the current heating temperature."""
|
36
|
+
async with pyplumio.open_tcp_connection("localhost", 8899) as conn:
|
37
|
+
print(conn.statistic)
|
38
|
+
|
39
|
+
|
40
|
+
asyncio.run(main())
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '0.5.
|
32
|
-
__version_tuple__ = version_tuple = (0, 5,
|
31
|
+
__version__ = version = '0.5.56'
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 56)
|
33
33
|
|
34
|
-
__commit_id__ = commit_id = '
|
34
|
+
__commit_id__ = commit_id = 'g61d991c97'
|
@@ -5,9 +5,12 @@ from __future__ import annotations
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
7
|
from collections.abc import Awaitable, Callable
|
8
|
-
from dataclasses import dataclass
|
8
|
+
from dataclasses import dataclass, field
|
9
|
+
from datetime import datetime
|
9
10
|
import logging
|
11
|
+
from typing import Any, Final, Literal
|
10
12
|
|
13
|
+
from dataslots import dataslots
|
11
14
|
from typing_extensions import TypeAlias
|
12
15
|
|
13
16
|
from pyplumio.const import ATTR_CONNECTED, ATTR_SETUP, DeviceType
|
@@ -23,6 +26,7 @@ from pyplumio.structures.network_info import (
|
|
23
26
|
NetworkInfo,
|
24
27
|
WirelessParameters,
|
25
28
|
)
|
29
|
+
from pyplumio.structures.regulator_data import ATTR_REGDATA
|
26
30
|
|
27
31
|
_LOGGER = logging.getLogger(__name__)
|
28
32
|
|
@@ -114,6 +118,87 @@ class Queues:
|
|
114
118
|
await asyncio.gather(self.read.join(), self.write.join())
|
115
119
|
|
116
120
|
|
121
|
+
NEVER: Final = "never"
|
122
|
+
|
123
|
+
|
124
|
+
@dataslots
|
125
|
+
@dataclass
|
126
|
+
class Statistics:
|
127
|
+
"""Represents a connection statistics.
|
128
|
+
|
129
|
+
:param received_bytes: Number of received bytes. Resets on reconnect.
|
130
|
+
:type received_bytes: int
|
131
|
+
:param received_frames: Number of received frames. Resets on reconnect.
|
132
|
+
:type received_frames: int
|
133
|
+
:param sent_bytes: Number of sent bytes. Resets on reconnect.
|
134
|
+
:type sent_bytes: int
|
135
|
+
:param sent_frames: Number of sent frames. Resets on reconnect.
|
136
|
+
:type sent_frames: int
|
137
|
+
:param failed_frames: Number of failed frames. Resets on reconnect.
|
138
|
+
:type failed_frames: int
|
139
|
+
:param connected_since: Datetime object representing connection time.
|
140
|
+
:type connected_since: datetime.datetime | Literal["never"]
|
141
|
+
:param connection_loss_at: Datetime object representing last connection loss event.
|
142
|
+
:type connection_loss_at: datetime.datetime | Literal["never"]
|
143
|
+
:param connection_losses: Number of connection lost event.
|
144
|
+
:type connection_losses: int
|
145
|
+
:param devices: Contains list of statistics for connected devices.
|
146
|
+
:type devices: list[DeviceStatistics]
|
147
|
+
"""
|
148
|
+
|
149
|
+
received_bytes: int = 0
|
150
|
+
received_frames: int = 0
|
151
|
+
sent_bytes: int = 0
|
152
|
+
sent_frames: int = 0
|
153
|
+
failed_frames: int = 0
|
154
|
+
connected_since: datetime | Literal["never"] = NEVER
|
155
|
+
connection_loss_at: datetime | Literal["never"] = NEVER
|
156
|
+
connection_losses: int = 0
|
157
|
+
devices: list[DeviceStatistics] = field(default_factory=list)
|
158
|
+
|
159
|
+
def update_transfer_statistics(
|
160
|
+
self, sent: Frame | None = None, received: Frame | None = None
|
161
|
+
) -> None:
|
162
|
+
"""Update transfer statistics."""
|
163
|
+
if sent:
|
164
|
+
self.sent_bytes += sent.length
|
165
|
+
self.sent_frames += 1
|
166
|
+
|
167
|
+
if received:
|
168
|
+
self.received_bytes += received.length
|
169
|
+
self.received_frames += 1
|
170
|
+
|
171
|
+
def reset_transfer_statistics(self) -> None:
|
172
|
+
"""Reset transfer statistics."""
|
173
|
+
self.sent_bytes = 0
|
174
|
+
self.sent_frames = 0
|
175
|
+
self.received_bytes = 0
|
176
|
+
self.received_frames = 0
|
177
|
+
self.failed_frames = 0
|
178
|
+
|
179
|
+
|
180
|
+
@dataslots
|
181
|
+
@dataclass
|
182
|
+
class DeviceStatistics:
|
183
|
+
"""Represents a device statistics.
|
184
|
+
|
185
|
+
:param name: Device name.
|
186
|
+
:type name: str
|
187
|
+
:param connected_since: Datetime object representing connection time.
|
188
|
+
:type connected_since: datetime.datetime | Literal["never"]
|
189
|
+
:param last_seen: Datetime object representing time when device was last seen.
|
190
|
+
:type last_seen: datetime.datetime | Literal["never"]
|
191
|
+
"""
|
192
|
+
|
193
|
+
name: str
|
194
|
+
connected_since: datetime | Literal["never"] = NEVER
|
195
|
+
last_seen: datetime | Literal["never"] = NEVER
|
196
|
+
|
197
|
+
async def update_last_seen(self, data: Any) -> None:
|
198
|
+
"""Update last seen property."""
|
199
|
+
self.last_seen = datetime.now()
|
200
|
+
|
201
|
+
|
117
202
|
class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
118
203
|
"""Represents an async protocol.
|
119
204
|
|
@@ -134,6 +219,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
134
219
|
_network: NetworkInfo
|
135
220
|
_queues: Queues
|
136
221
|
_entry_lock: asyncio.Lock
|
222
|
+
_statistics: Statistics
|
137
223
|
|
138
224
|
def __init__(
|
139
225
|
self,
|
@@ -150,6 +236,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
150
236
|
)
|
151
237
|
self._queues = Queues(read=asyncio.Queue(), write=asyncio.Queue())
|
152
238
|
self._entry_lock = asyncio.Lock()
|
239
|
+
self._statistics = Statistics()
|
153
240
|
|
154
241
|
def connection_established(
|
155
242
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
@@ -172,6 +259,8 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
172
259
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
173
260
|
|
174
261
|
self.connected.set()
|
262
|
+
self.statistics.reset_transfer_statistics()
|
263
|
+
self.statistics.connected_since = datetime.now()
|
175
264
|
|
176
265
|
async def _connection_close(self) -> None:
|
177
266
|
"""Close the connection if it is established."""
|
@@ -200,19 +289,27 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
200
289
|
self, queues: Queues, reader: FrameReader, writer: FrameWriter
|
201
290
|
) -> None:
|
202
291
|
"""Handle frame reads and writes."""
|
292
|
+
statistics = self.statistics
|
203
293
|
await self.connected.wait()
|
204
294
|
while self.connected.is_set():
|
205
295
|
try:
|
296
|
+
request = None
|
206
297
|
if not queues.write.empty():
|
207
|
-
|
298
|
+
request = await queues.write.get()
|
299
|
+
await writer.write(request)
|
208
300
|
queues.write.task_done()
|
209
301
|
|
210
302
|
if response := await reader.read():
|
211
303
|
queues.read.put_nowait(response)
|
212
304
|
|
305
|
+
statistics.update_transfer_statistics(request, response)
|
306
|
+
|
213
307
|
except ProtocolError as e:
|
308
|
+
statistics.failed_frames += 1
|
214
309
|
_LOGGER.debug("Can't process received frame: %s", e)
|
215
310
|
except (OSError, asyncio.TimeoutError):
|
311
|
+
statistics.connection_losses += 1
|
312
|
+
statistics.connection_loss_at = datetime.now()
|
216
313
|
self.create_task(self.connection_lost())
|
217
314
|
break
|
218
315
|
except Exception:
|
@@ -239,8 +336,21 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
239
336
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
240
337
|
device.dispatch_nowait(ATTR_SETUP, True)
|
241
338
|
await self.dispatch(name, device)
|
339
|
+
self.statistics.devices.append(
|
340
|
+
device_statistics := DeviceStatistics(
|
341
|
+
name=name,
|
342
|
+
connected_since=datetime.now(),
|
343
|
+
last_seen=datetime.now(),
|
344
|
+
)
|
345
|
+
)
|
346
|
+
device.subscribe(ATTR_REGDATA, device_statistics.update_last_seen)
|
242
347
|
|
243
348
|
return self.data[name]
|
244
349
|
|
350
|
+
@property
|
351
|
+
def statistics(self) -> Statistics:
|
352
|
+
"""Return the statistics."""
|
353
|
+
return self._statistics
|
354
|
+
|
245
355
|
|
246
|
-
__all__ = ["Protocol", "DummyProtocol", "AsyncProtocol"]
|
356
|
+
__all__ = ["Protocol", "DummyProtocol", "AsyncProtocol", "Statistics"]
|
@@ -1,7 +1,10 @@
|
|
1
1
|
"""Contains tests for the protocol classes."""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
from dataclasses import asdict
|
5
|
+
from datetime import datetime, timedelta
|
4
6
|
import logging
|
7
|
+
from typing import cast
|
5
8
|
from unittest.mock import AsyncMock, Mock, PropertyMock, call, patch
|
6
9
|
|
7
10
|
import pytest
|
@@ -15,19 +18,20 @@ from pyplumio.exceptions import (
|
|
15
18
|
UnknownDeviceError,
|
16
19
|
UnknownFrameError,
|
17
20
|
)
|
18
|
-
from pyplumio.frames import Response
|
21
|
+
from pyplumio.frames import Request, Response
|
19
22
|
from pyplumio.frames.requests import (
|
20
23
|
CheckDeviceRequest,
|
21
24
|
ProgramVersionRequest,
|
22
25
|
StartMasterRequest,
|
23
26
|
)
|
24
|
-
from pyplumio.protocol import AsyncProtocol, DummyProtocol, Queues
|
27
|
+
from pyplumio.protocol import AsyncProtocol, DummyProtocol, Queues, Statistics
|
25
28
|
from pyplumio.stream import FrameReader, FrameWriter
|
26
29
|
from pyplumio.structures.network_info import (
|
27
30
|
EthernetParameters,
|
28
31
|
NetworkInfo,
|
29
32
|
WirelessParameters,
|
30
33
|
)
|
34
|
+
from pyplumio.structures.regulator_data import ATTR_REGDATA
|
31
35
|
|
32
36
|
|
33
37
|
@pytest.fixture(name="skip_asyncio_create_task")
|
@@ -102,9 +106,30 @@ async def test_dummy_protocol() -> None:
|
|
102
106
|
assert not dummy_protocol.connected.is_set()
|
103
107
|
|
104
108
|
|
109
|
+
def test_statistics() -> None:
|
110
|
+
"""Test statistics dataclass."""
|
111
|
+
statistics = Statistics()
|
112
|
+
statistics.update_transfer_statistics(sent=Request())
|
113
|
+
statistics.update_transfer_statistics(received=Response())
|
114
|
+
statistics.failed_frames = 1
|
115
|
+
assert statistics.connected_since == "never"
|
116
|
+
assert statistics.sent_bytes == 10
|
117
|
+
assert statistics.sent_frames == 1
|
118
|
+
assert statistics.received_bytes == 10
|
119
|
+
assert statistics.received_frames == 1
|
120
|
+
assert statistics.failed_frames == 1
|
121
|
+
statistics.reset_transfer_statistics()
|
122
|
+
assert statistics.sent_bytes == 0
|
123
|
+
assert statistics.sent_frames == 0
|
124
|
+
assert statistics.received_bytes == 0
|
125
|
+
assert statistics.received_frames == 0
|
126
|
+
assert statistics.failed_frames == 0
|
127
|
+
|
128
|
+
|
105
129
|
@patch("pyplumio.protocol.AsyncProtocol.create_task")
|
106
130
|
@patch("pyplumio.protocol.AsyncProtocol.frame_consumer", new_callable=Mock)
|
107
131
|
@patch("pyplumio.protocol.AsyncProtocol.frame_producer", new_callable=Mock)
|
132
|
+
@pytest.mark.usefixtures("frozen_time")
|
108
133
|
def test_async_protocol_connection_established(
|
109
134
|
mock_frame_producer, mock_frame_consumer, mock_create_task
|
110
135
|
) -> None:
|
@@ -124,8 +149,13 @@ def test_async_protocol_connection_established(
|
|
124
149
|
async_protocol.data = {"ecomax": mock_ecomax}
|
125
150
|
|
126
151
|
# Test connection established.
|
127
|
-
with
|
128
|
-
|
152
|
+
with (
|
153
|
+
patch.object(
|
154
|
+
async_protocol, "_queues", Queues(mock_read_queue, mock_write_queue)
|
155
|
+
),
|
156
|
+
patch(
|
157
|
+
"pyplumio.protocol.Statistics.reset_transfer_statistics"
|
158
|
+
) as mock_reset_transfer_statistics,
|
129
159
|
):
|
130
160
|
async_protocol.connection_established(mock_stream_reader, mock_stream_writer)
|
131
161
|
|
@@ -144,6 +174,10 @@ def test_async_protocol_connection_established(
|
|
144
174
|
# Check that devices were notified.
|
145
175
|
mock_ecomax.dispatch_nowait.assert_called_once_with(ATTR_CONNECTED, True)
|
146
176
|
|
177
|
+
# Check statistics.
|
178
|
+
mock_reset_transfer_statistics.assert_called_once()
|
179
|
+
assert async_protocol.statistics.connected_since == datetime.now()
|
180
|
+
|
147
181
|
|
148
182
|
async def test_async_protocol_connection_lost() -> None:
|
149
183
|
"""Test losing the connection with an async protocol."""
|
@@ -236,7 +270,9 @@ async def test_async_protocol_shutdown(
|
|
236
270
|
assert async_protocol.writer is None
|
237
271
|
|
238
272
|
|
239
|
-
@pytest.mark.usefixtures(
|
273
|
+
@pytest.mark.usefixtures(
|
274
|
+
"skip_asyncio_events", "skip_asyncio_create_task", "frozen_time"
|
275
|
+
)
|
240
276
|
async def test_async_protocol_frame_producer(
|
241
277
|
async_protocol: AsyncProtocol, caplog
|
242
278
|
) -> None:
|
@@ -267,7 +303,8 @@ async def test_async_protocol_frame_producer(
|
|
267
303
|
*(True for _ in range(len(responses) - 1)),
|
268
304
|
)
|
269
305
|
)
|
270
|
-
|
306
|
+
request = Request()
|
307
|
+
mock_write_queue.get = AsyncMock(return_value=request)
|
271
308
|
|
272
309
|
with (
|
273
310
|
patch("pyplumio.devices.ecomax.EcoMAX"),
|
@@ -315,7 +352,7 @@ async def test_async_protocol_frame_producer(
|
|
315
352
|
),
|
316
353
|
]
|
317
354
|
|
318
|
-
mock_writer.write.assert_awaited_once_with(
|
355
|
+
mock_writer.write.assert_awaited_once_with(request)
|
319
356
|
mock_write_queue.task_done.assert_called_once()
|
320
357
|
mock_read_queue.put_nowait.assert_called_once_with(success)
|
321
358
|
mock_connection_lost.assert_called_once()
|
@@ -323,6 +360,19 @@ async def test_async_protocol_frame_producer(
|
|
323
360
|
assert mock_write_queue.empty.call_count == 8
|
324
361
|
assert mock_reader.read.await_count == 8
|
325
362
|
|
363
|
+
# Check statistics.
|
364
|
+
assert asdict(async_protocol.statistics) == {
|
365
|
+
"connected_since": "never",
|
366
|
+
"received_bytes": 10,
|
367
|
+
"received_frames": 1,
|
368
|
+
"sent_bytes": 10,
|
369
|
+
"sent_frames": 1,
|
370
|
+
"failed_frames": 5,
|
371
|
+
"connection_losses": 1,
|
372
|
+
"connection_loss_at": datetime.now(),
|
373
|
+
"devices": [],
|
374
|
+
}
|
375
|
+
|
326
376
|
|
327
377
|
@patch("pyplumio.frames.requests.CheckDeviceRequest.response")
|
328
378
|
@patch("pyplumio.frames.requests.ProgramVersionRequest.response")
|
@@ -332,6 +382,7 @@ async def test_async_protocol_frame_consumer(
|
|
332
382
|
mock_device_available_response,
|
333
383
|
async_protocol: AsyncProtocol,
|
334
384
|
caplog,
|
385
|
+
frozen_time,
|
335
386
|
) -> None:
|
336
387
|
"""Test a frame consumer task within an async protocol."""
|
337
388
|
mock_read_queue = Mock(spec=asyncio.Queue)
|
@@ -394,3 +445,14 @@ async def test_async_protocol_frame_consumer(
|
|
394
445
|
]
|
395
446
|
)
|
396
447
|
assert mock_read_queue.task_done.call_count == 2
|
448
|
+
|
449
|
+
# Test statistics.
|
450
|
+
connected_since = datetime.now()
|
451
|
+
frozen_time.tick(timedelta(seconds=10))
|
452
|
+
ecomax = cast(EcoMAX, async_protocol.get_nowait("ecomax"))
|
453
|
+
await ecomax.dispatch(ATTR_REGDATA, True)
|
454
|
+
assert asdict(async_protocol.statistics.devices[0]) == {
|
455
|
+
"name": "ecomax",
|
456
|
+
"connected_since": connected_since,
|
457
|
+
"last_seen": datetime.now(),
|
458
|
+
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|