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 +2 -2
- pyplumio/connection.py +0 -36
- pyplumio/protocol.py +50 -40
- {pyplumio-0.6.0.dist-info → pyplumio-0.6.1.dist-info}/METADATA +6 -7
- {pyplumio-0.6.0.dist-info → pyplumio-0.6.1.dist-info}/RECORD +8 -8
- {pyplumio-0.6.0.dist-info → pyplumio-0.6.1.dist-info}/WHEEL +0 -0
- {pyplumio-0.6.0.dist-info → pyplumio-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.6.0.dist-info → pyplumio-0.6.1.dist-info}/top_level.txt +0 -0
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.
|
32
|
-
__version_tuple__ = version_tuple = (0, 6,
|
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:
|
148
|
+
devices: set[DeviceStatistics] = field(default_factory=set)
|
149
149
|
|
150
|
-
def
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
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
|
163
|
-
"""
|
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
|
181
|
-
|
184
|
+
#: Device address
|
185
|
+
address: int
|
182
186
|
|
183
187
|
#: Datetime object representing connection time
|
184
|
-
connected_since: datetime
|
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
|
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
|
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 ({
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
35
|
-
Requires-Dist: ruff==0.13.
|
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.
|
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=
|
4
|
-
pyplumio/connection.py,sha256=
|
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=
|
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.
|
60
|
-
pyplumio-0.6.
|
61
|
-
pyplumio-0.6.
|
62
|
-
pyplumio-0.6.
|
63
|
-
pyplumio-0.6.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|