PyPlumIO 0.6.0__py3-none-any.whl → 0.6.1__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.6.0'
32
- __version_tuple__ = version_tuple = (0, 6, 0)
31
+ __version__ = version = '0.6.1'
32
+ __version_tuple__ = version_tuple = (0, 6, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
pyplumio/connection.py CHANGED
@@ -116,42 +116,6 @@ class Connection(ABC, TaskManager):
116
116
 
117
117
  yield await self.protocol.get(name, timeout=timeout)
118
118
 
119
- @property
120
- def get(self): # type: ignore[no-untyped-def]
121
- """Access the remote device.
122
-
123
- Raise NotImplementedError when using protocol
124
- different from AsyncProtocol.
125
- """
126
- if isinstance(self.protocol, AsyncProtocol):
127
- return self.protocol.get
128
-
129
- raise NotImplementedError
130
-
131
- @property
132
- def get_nowait(self): # type: ignore[no-untyped-def]
133
- """Access the remote device without waiting.
134
-
135
- Raise NotImplementedError when using protocol
136
- different from AsyncProtocol.
137
- """
138
- if isinstance(self.protocol, AsyncProtocol):
139
- return self.protocol.get_nowait
140
-
141
- raise NotImplementedError
142
-
143
- @property
144
- def wait_for(self): # type: ignore[no-untyped-def]
145
- """Wait for the remote device to become available.
146
-
147
- Raise NotImplementedError when using protocol
148
- different from AsyncProtocol.
149
- """
150
- if isinstance(self.protocol, AsyncProtocol):
151
- return self.protocol.wait_for
152
-
153
- raise NotImplementedError
154
-
155
119
  @property
156
120
  def protocol(self) -> Protocol:
157
121
  """Return the protocol object."""
pyplumio/protocol.py CHANGED
@@ -145,25 +145,29 @@ class Statistics:
145
145
  connection_losses: int = 0
146
146
 
147
147
  #: List of statistics for connected devices
148
- devices: list[DeviceStatistics] = field(default_factory=list)
148
+ devices: set[DeviceStatistics] = field(default_factory=set)
149
149
 
150
- def update_transfer_statistics(
151
- self, sent: Frame | None = None, received: Frame | None = None
152
- ) -> None:
153
- """Update transfer statistics."""
154
- if sent:
155
- self.sent_bytes += sent.length
156
- self.sent_frames += 1
150
+ def update_sent(self, frame: Frame) -> None:
151
+ """Update sent frames statistics."""
152
+ self.sent_bytes += frame.length
153
+ self.sent_frames += 1
157
154
 
158
- if received:
159
- self.received_bytes += received.length
160
- self.received_frames += 1
155
+ def update_received(self, frame: Frame) -> None:
156
+ """Update received frames statistics."""
157
+ self.received_bytes += frame.length
158
+ self.received_frames += 1
161
159
 
162
- def track_connection_loss(self) -> None:
163
- """Increase connection loss counter and store the datetime."""
160
+ def update_connection_lost(self) -> None:
161
+ """Update connection lost counter."""
164
162
  self.connection_losses += 1
165
163
  self.connection_loss_at = datetime.now()
166
164
 
165
+ def update_devices(self, device: PhysicalDevice) -> None:
166
+ """Update connected devices."""
167
+ device_statistics = DeviceStatistics(address=device.address)
168
+ device.subscribe(ATTR_REGDATA, device_statistics.update_last_seen)
169
+ self.devices.add(device_statistics)
170
+
167
171
  def reset_transfer_statistics(self) -> None:
168
172
  """Reset transfer statistics."""
169
173
  self.sent_bytes = 0
@@ -173,18 +177,22 @@ class Statistics:
173
177
  self.failed_frames = 0
174
178
 
175
179
 
176
- @dataclass(slots=True)
180
+ @dataclass(slots=True, kw_only=True)
177
181
  class DeviceStatistics:
178
182
  """Represents a device statistics."""
179
183
 
180
- #: Device name
181
- name: str
184
+ #: Device address
185
+ address: int
182
186
 
183
187
  #: Datetime object representing connection time
184
- connected_since: datetime | Literal["never"] = NEVER
188
+ connected_since: datetime = field(default_factory=datetime.now)
185
189
 
186
190
  #: Datetime object representing time when device was last seen
187
- last_seen: datetime | Literal["never"] = NEVER
191
+ last_seen: datetime = field(default_factory=datetime.now)
192
+
193
+ def __hash__(self) -> int:
194
+ """Return a hash of the statistics based on unique address."""
195
+ return self.address
188
196
 
189
197
  async def update_last_seen(self, _: Any) -> None:
190
198
  """Update last seen property."""
@@ -241,10 +249,10 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
241
249
  self.frame_producer(self._queues, reader=self.reader, writer=self.writer),
242
250
  name="frame_producer_task",
243
251
  )
244
- for consumer in range(self.consumers_count):
252
+ for consumer_id in range(self.consumers_count):
245
253
  self.create_task(
246
254
  self.frame_consumer(self._queues.read),
247
- name=f"frame_consumer_task ({consumer})",
255
+ name=f"frame_consumer_task ({consumer_id})",
248
256
  )
249
257
 
250
258
  for device in self.data.values():
@@ -277,30 +285,39 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
277
285
  await self._connection_close()
278
286
  await asyncio.gather(*(device.shutdown() for device in self.data.values()))
279
287
 
288
+ async def _write_from_queue(
289
+ self, writer: FrameWriter, queue: asyncio.Queue[Frame]
290
+ ) -> None:
291
+ """Send frame from the queue to the remote device."""
292
+ frame = await queue.get()
293
+ await writer.write(frame)
294
+ queue.task_done()
295
+ self.statistics.update_sent(frame)
296
+
297
+ async def _read_into_queue(
298
+ self, reader: FrameReader, queue: asyncio.Queue[Frame]
299
+ ) -> None:
300
+ """Read frame from the remote device into the queue."""
301
+ if frame := await reader.read():
302
+ queue.put_nowait(frame)
303
+ self.statistics.update_received(frame)
304
+
280
305
  async def frame_producer(
281
306
  self, queues: Queues, reader: FrameReader, writer: FrameWriter
282
307
  ) -> None:
283
308
  """Handle frame reads and writes."""
284
- statistics = self.statistics
285
309
  await self.connected.wait()
286
310
  while self.connected.is_set():
287
311
  try:
288
- request = None
289
312
  if not queues.write.empty():
290
- request = await queues.write.get()
291
- await writer.write(request)
292
- queues.write.task_done()
293
-
294
- if response := await reader.read():
295
- queues.read.put_nowait(response)
296
-
297
- statistics.update_transfer_statistics(request, response)
313
+ await self._write_from_queue(writer, queues.write)
298
314
 
315
+ await self._read_into_queue(reader, queues.read)
299
316
  except ProtocolError as e:
300
- statistics.failed_frames += 1
317
+ self.statistics.failed_frames += 1
301
318
  _LOGGER.debug("Can't process received frame: %s", e)
302
319
  except (OSError, asyncio.TimeoutError):
303
- statistics.track_connection_loss()
320
+ self.statistics.update_connection_lost()
304
321
  self.create_task(self.connection_lost())
305
322
  break
306
323
  except Exception:
@@ -327,14 +344,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
327
344
  device.dispatch_nowait(ATTR_CONNECTED, True)
328
345
  device.dispatch_nowait(ATTR_SETUP, True)
329
346
  await self.dispatch(name, device)
330
- self.statistics.devices.append(
331
- device_statistics := DeviceStatistics(
332
- name=name,
333
- connected_since=datetime.now(),
334
- last_seen=datetime.now(),
335
- )
336
- )
337
- device.subscribe(ATTR_REGDATA, device_statistics.update_last_seen)
347
+ self.statistics.update_devices(device)
338
348
 
339
349
  return self.data[name]
340
350
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.6.0
3
+ Version: 0.6.1
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
@@ -25,16 +25,16 @@ License-File: LICENSE
25
25
  Requires-Dist: pyserial-asyncio==0.6
26
26
  Provides-Extra: test
27
27
  Requires-Dist: codespell==2.4.1; extra == "test"
28
- Requires-Dist: coverage==7.10.6; extra == "test"
28
+ Requires-Dist: coverage==7.10.7; extra == "test"
29
29
  Requires-Dist: freezegun==1.5.5; extra == "test"
30
- Requires-Dist: mypy==1.18.1; extra == "test"
30
+ Requires-Dist: mypy==1.18.2; extra == "test"
31
31
  Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
32
32
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
33
33
  Requires-Dist: pytest==8.4.2; extra == "test"
34
- Requires-Dist: pytest-asyncio==1.1.0; extra == "test"
35
- Requires-Dist: ruff==0.13.0; extra == "test"
34
+ Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
35
+ Requires-Dist: ruff==0.13.2; extra == "test"
36
36
  Requires-Dist: tox==4.30.2; extra == "test"
37
- Requires-Dist: types-pyserial==3.5.0.20250822; extra == "test"
37
+ Requires-Dist: types-pyserial==3.5.0.20250919; extra == "test"
38
38
  Provides-Extra: docs
39
39
  Requires-Dist: sphinx==8.1.3; extra == "docs"
40
40
  Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
@@ -79,7 +79,6 @@ through network by using RS-485 to Ethernet/WiFi converter.
79
79
  - [Callbacks](https://pyplumio.denpa.pro/callbacks.html)
80
80
  - [Mixers/Thermostats](https://pyplumio.denpa.pro/mixers_thermostats.html)
81
81
  - [Schedules](https://pyplumio.denpa.pro/schedules.html)
82
- - [Statistics](https://pyplumio.denpa.pro/statistics.html)
83
82
  - [Protocol](https://pyplumio.denpa.pro/protocol.html)
84
83
  - [Frame Structure](https://pyplumio.denpa.pro/protocol.html#frame-structure)
85
84
  - [Requests and Responses](https://pyplumio.denpa.pro/protocol.html#requests-and-responses)
@@ -1,12 +1,12 @@
1
1
  pyplumio/__init__.py,sha256=DQg-ZTAxLYuNKyqsGrcO0QrVAMw9aPA69Bv2mZ7ubXQ,3314
2
2
  pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
3
- pyplumio/_version.py,sha256=MAYWefOLb6kbIRub18WSzK6ggSjz1LNLy9aDRlX9Ea4,704
4
- pyplumio/connection.py,sha256=9Gzg6FXMU-HjspsDnm9XH8ZPBO29AZ6dKS2-eg8P8Z0,7686
3
+ pyplumio/_version.py,sha256=7vNQiXfKffK0nbqts6Xy6-E1b1YOm4EGigvgaHr83o4,704
4
+ pyplumio/connection.py,sha256=4JoutupQSvAO8WXFFuwddpJJODzna5oq-cHJRI4kgZ8,6625
5
5
  pyplumio/const.py,sha256=oYwXB3N6bvFLc6411icbABbBkSoQcj5BGuyD-NaKYp8,5629
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=sBEnr0i_1XMbIwIEA24npbpe5yevSRneynlsqJMyfko,15642
9
- pyplumio/protocol.py,sha256=jGx5b8y_1jbdFbjL_ZbUjpDvgBYhn5JUBsVsf_De6Ls,11614
9
+ pyplumio/protocol.py,sha256=u0MqTFwbKv-kJeokXPgF2pNTVcFUExkH9MWIcY8ak7c,12083
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=SV47Y6QC_KL-gPmk6KQgx7ArExzNHGKuaddGAHjT9rs,1839
@@ -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.6.0.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
60
- pyplumio-0.6.0.dist-info/METADATA,sha256=fyZvRedY6toO1OuENZe4ytYtPA0EFJqYb7CJy-Tswlo,5579
61
- pyplumio-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
- pyplumio-0.6.0.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
63
- pyplumio-0.6.0.dist-info/RECORD,,
59
+ pyplumio-0.6.1.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
60
+ pyplumio-0.6.1.dist-info/METADATA,sha256=kIPXgPoGlOwVLdy5Va7UJjGg4yBorcNv9IJeof7ABfM,5520
61
+ pyplumio-0.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
+ pyplumio-0.6.1.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
63
+ pyplumio-0.6.1.dist-info/RECORD,,