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.
Files changed (172) hide show
  1. {pyplumio-0.5.55 → pyplumio-0.5.56}/PKG-INFO +1 -1
  2. {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/PKG-INFO +1 -1
  3. {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/SOURCES.txt +1 -0
  4. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/index.rst +1 -0
  5. pyplumio-0.5.56/docs/source/statistics.rst +40 -0
  6. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/_version.py +3 -3
  7. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/protocol.py +113 -3
  8. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_protocol.py +69 -7
  9. {pyplumio-0.5.55 → pyplumio-0.5.56}/.gitattributes +0 -0
  10. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/CODE_OF_CONDUCT.md +0 -0
  11. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/dependabot.yml +0 -0
  14. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/ci.yml +0 -0
  15. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/codeql.yml +0 -0
  16. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/deploy.yml +0 -0
  17. {pyplumio-0.5.55 → pyplumio-0.5.56}/.github/workflows/documentation.yml +0 -0
  18. {pyplumio-0.5.55 → pyplumio-0.5.56}/.gitignore +0 -0
  19. {pyplumio-0.5.55 → pyplumio-0.5.56}/.pre-commit-config.yaml +0 -0
  20. {pyplumio-0.5.55 → pyplumio-0.5.56}/.qlty/qlty.toml +0 -0
  21. {pyplumio-0.5.55 → pyplumio-0.5.56}/.vscode/settings.json +0 -0
  22. {pyplumio-0.5.55 → pyplumio-0.5.56}/LICENSE +0 -0
  23. {pyplumio-0.5.55 → pyplumio-0.5.56}/MANIFEST.in +0 -0
  24. {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  25. {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/requires.txt +0 -0
  26. {pyplumio-0.5.55 → pyplumio-0.5.56}/PyPlumIO.egg-info/top_level.txt +0 -0
  27. {pyplumio-0.5.55 → pyplumio-0.5.56}/README.md +0 -0
  28. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/Makefile +0 -0
  29. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/make.bat +0 -0
  30. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/callbacks.rst +0 -0
  31. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/conf.py +0 -0
  32. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/connecting.rst +0 -0
  33. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/frames.rst +0 -0
  34. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/mixers_thermostats.rst +0 -0
  35. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/protocol.rst +0 -0
  36. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/reading.rst +0 -0
  37. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/schedules.rst +0 -0
  38. {pyplumio-0.5.55 → pyplumio-0.5.56}/docs/source/writing.rst +0 -0
  39. {pyplumio-0.5.55 → pyplumio-0.5.56}/images/ecomax.png +0 -0
  40. {pyplumio-0.5.55 → pyplumio-0.5.56}/images/rs485.png +0 -0
  41. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/__init__.py +0 -0
  42. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/__main__.py +0 -0
  43. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/connection.py +0 -0
  44. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/const.py +0 -0
  45. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/data_types.py +0 -0
  46. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/__init__.py +0 -0
  47. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/ecomax.py +0 -0
  48. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/ecoster.py +0 -0
  49. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/mixer.py +0 -0
  50. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/devices/thermostat.py +0 -0
  51. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/exceptions.py +0 -0
  52. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/filters.py +0 -0
  53. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/__init__.py +0 -0
  54. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/messages.py +0 -0
  55. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/requests.py +0 -0
  56. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/frames/responses.py +0 -0
  57. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/__init__.py +0 -0
  58. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/async_cache.py +0 -0
  59. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/event_manager.py +0 -0
  60. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/factory.py +0 -0
  61. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/helpers/task_manager.py +0 -0
  62. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/__init__.py +0 -0
  63. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/custom/__init__.py +0 -0
  64. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -0
  65. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/ecomax.py +0 -0
  66. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/mixer.py +0 -0
  67. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/parameters/thermostat.py +0 -0
  68. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/py.typed +0 -0
  69. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/stream.py +0 -0
  70. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/__init__.py +0 -0
  71. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/alerts.py +0 -0
  72. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/boiler_load.py +0 -0
  73. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/boiler_power.py +0 -0
  74. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/ecomax_parameters.py +0 -0
  75. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/fan_power.py +0 -0
  76. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/frame_versions.py +0 -0
  77. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/fuel_consumption.py +0 -0
  78. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/fuel_level.py +0 -0
  79. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/lambda_sensor.py +0 -0
  80. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/mixer_parameters.py +0 -0
  81. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/mixer_sensors.py +0 -0
  82. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/modules.py +0 -0
  83. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/network_info.py +0 -0
  84. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/output_flags.py +0 -0
  85. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/outputs.py +0 -0
  86. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/pending_alerts.py +0 -0
  87. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/product_info.py +0 -0
  88. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/program_version.py +0 -0
  89. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/regulator_data.py +0 -0
  90. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/regulator_data_schema.py +0 -0
  91. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/schedules.py +0 -0
  92. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/statuses.py +0 -0
  93. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/temperatures.py +0 -0
  94. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/thermostat_parameters.py +0 -0
  95. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/structures/thermostat_sensors.py +0 -0
  96. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyplumio/utils.py +0 -0
  97. {pyplumio-0.5.55 → pyplumio-0.5.56}/pyproject.toml +0 -0
  98. {pyplumio-0.5.55 → pyplumio-0.5.56}/requirements.txt +0 -0
  99. {pyplumio-0.5.55 → pyplumio-0.5.56}/requirements_docs.txt +0 -0
  100. {pyplumio-0.5.55 → pyplumio-0.5.56}/requirements_test.txt +0 -0
  101. {pyplumio-0.5.55 → pyplumio-0.5.56}/setup.cfg +0 -0
  102. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/__init__.py +0 -0
  103. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/conftest.py +0 -0
  104. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/__init__.py +0 -0
  105. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_ecomax.py +0 -0
  106. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_ecoster.py +0 -0
  107. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_init.py +0 -0
  108. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_mixer.py +0 -0
  109. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/devices/test_thermostat.py +0 -0
  110. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_init.py +0 -0
  111. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_messages.py +0 -0
  112. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_requests.py +0 -0
  113. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/frames/test_responses.py +0 -0
  114. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/__init__.py +0 -0
  115. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_async_cache.py +0 -0
  116. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_event_manager.py +0 -0
  117. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_factory.py +0 -0
  118. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_task_manager.py +0 -0
  119. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/helpers/test_uid.py +0 -0
  120. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/__init__.py +0 -0
  121. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/custom/__init__.py +0 -0
  122. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/custom/test_ecomax_860d3_hb.py +0 -0
  123. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/custom/test_init.py +0 -0
  124. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_ecomax.py +0 -0
  125. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_init.py +0 -0
  126. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_mixers.py +0 -0
  127. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/parameters/test_thermostats.py +0 -0
  128. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/ruff.toml +0 -0
  129. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/__init__.py +0 -0
  130. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_alerts.py +0 -0
  131. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_boiler_load.py +0 -0
  132. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_boiler_power.py +0 -0
  133. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_ecomax_parameters.py +0 -0
  134. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_fan_power.py +0 -0
  135. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_frame_versions.py +0 -0
  136. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_fuel_consumption.py +0 -0
  137. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_fuel_level.py +0 -0
  138. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_lambda_sensor.py +0 -0
  139. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_mixer_parameters.py +0 -0
  140. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_product_info.py +0 -0
  141. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/structures/test_schedules.py +0 -0
  142. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_connection.py +0 -0
  143. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_data_types.py +0 -0
  144. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_filters.py +0 -0
  145. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_init.py +0 -0
  146. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_main.py +0 -0
  147. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_stream.py +0 -0
  148. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/test_utils.py +0 -0
  149. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/messages/regulator_data.json +0 -0
  150. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/messages/sensor_data.json +0 -0
  151. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/parameters/ecomax_860d3_hb.json +0 -0
  152. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/alerts.json +0 -0
  153. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/ecomax_control.json +0 -0
  154. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/ecomax_parameters.json +0 -0
  155. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/mixer_parameters.json +0 -0
  156. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  157. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  158. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_schedule.json +0 -0
  159. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  160. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/requests/thermostat_parameters.json +0 -0
  161. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/alerts.json +0 -0
  162. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/device_available.json +0 -0
  163. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/ecomax_parameters.json +0 -0
  164. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/mixer_parameters.json +0 -0
  165. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/password.json +0 -0
  166. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/program_version.json +0 -0
  167. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/regulator_data_schema.json +0 -0
  168. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/schedules.json +0 -0
  169. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/thermostat_parameters.json +0 -0
  170. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/responses/uid.json +0 -0
  171. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  172. {pyplumio-0.5.55 → pyplumio-0.5.56}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.55
3
+ Version: 0.5.56
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.55
3
+ Version: 0.5.56
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
@@ -34,6 +34,7 @@ docs/source/mixers_thermostats.rst
34
34
  docs/source/protocol.rst
35
35
  docs/source/reading.rst
36
36
  docs/source/schedules.rst
37
+ docs/source/statistics.rst
37
38
  docs/source/writing.rst
38
39
  images/ecomax.png
39
40
  images/rs485.png
@@ -51,6 +51,7 @@ Documentation
51
51
  callbacks
52
52
  mixers_thermostats
53
53
  schedules
54
+ statistics
54
55
  protocol
55
56
  frames
56
57
 
@@ -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.55'
32
- __version_tuple__ = version_tuple = (0, 5, 55)
31
+ __version__ = version = '0.5.56'
32
+ __version_tuple__ = version_tuple = (0, 5, 56)
33
33
 
34
- __commit_id__ = commit_id = 'gb0f1bcbdf'
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
- await writer.write(await queues.write.get())
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 patch.object(
128
- async_protocol, "_queues", Queues(mock_read_queue, mock_write_queue)
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("skip_asyncio_events", "skip_asyncio_create_task")
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
- mock_write_queue.get = AsyncMock(return_value="test_request")
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("test_request")
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