PyPlumIO 0.5.55__py3-none-any.whl → 0.5.56__py3-none-any.whl

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/_version.py CHANGED
@@ -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
34
  __commit_id__ = commit_id = None
pyplumio/protocol.py CHANGED
@@ -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,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,12 +1,12 @@
1
1
  pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
2
2
  pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
3
- pyplumio/_version.py,sha256=G1TzBNDMFivoxxcx0zhCWDpZU92nh3Fwl-CwzCVn15g,706
3
+ pyplumio/_version.py,sha256=AF48JunwQGeZYZUVpG4oP5XCtgX_ssZWD1yfHXBvb9I,706
4
4
  pyplumio/connection.py,sha256=u-iOzEUqoEEL4YLpLtzBWi5Qy8_RABgKD8DyXf-er-4,5892
5
5
  pyplumio/const.py,sha256=eoq-WNJ8TO3YlP7dC7KkVQRKGjt9FbRZ6M__s29vb1U,5659
6
6
  pyplumio/data_types.py,sha256=BTDxwErRo_odvFT5DNfIniNh8ZfyjRKEDaJmoEJqdEg,9426
7
7
  pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
8
8
  pyplumio/filters.py,sha256=QEtOptXym2Fb82cdPpS1dajkTpvYi3VuQaYoLl4CSQ4,15658
9
- pyplumio/protocol.py,sha256=DWM-yJnm2EQPLvGzXNlkQ0IpKQn44e-WkNB_DqZAag8,8313
9
+ pyplumio/protocol.py,sha256=ndsdnd7juX-NlrBMIAhEkx8x5DNlh_Q4FZ4pzvbT1yQ,12276
10
10
  pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  pyplumio/stream.py,sha256=zFMKZ_GxsSGcaBTJigVM1CK3uGjlEJXgcvKqus8MDzk,7740
12
12
  pyplumio/utils.py,sha256=ktV8_Th2DiwQ0W6afOCau9kBJ8pOrqR-SM2Y2GRy-xE,1869
@@ -56,8 +56,8 @@ pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8p
56
56
  pyplumio/structures/temperatures.py,sha256=2VD3P_vwp9PEBkOn2-WhifOR8w-UYNq35aAxle0z2Vg,2831
57
57
  pyplumio/structures/thermostat_parameters.py,sha256=st3x3HkjQm3hqBrn_fpvPDQu8fuc-Sx33ONB19ViQak,3007
58
58
  pyplumio/structures/thermostat_sensors.py,sha256=rO9jTZWGQpThtJqVdbbv8sYMYHxJi4MfwZQza69L2zw,3399
59
- pyplumio-0.5.55.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
60
- pyplumio-0.5.55.dist-info/METADATA,sha256=85fX-7JuFsPkNo-zO8RJrM-m82-c_V3eJHr6pMNblOk,5617
61
- pyplumio-0.5.55.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
- pyplumio-0.5.55.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
63
- pyplumio-0.5.55.dist-info/RECORD,,
59
+ pyplumio-0.5.56.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
60
+ pyplumio-0.5.56.dist-info/METADATA,sha256=2YzNOAb-PMDVCsLFfR0VLgsPGnQWGzOXL6nzuCTncTk,5617
61
+ pyplumio-0.5.56.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
+ pyplumio-0.5.56.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
63
+ pyplumio-0.5.56.dist-info/RECORD,,