PyPlumIO 0.5.54__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 +2 -2
- pyplumio/devices/ecomax.py +14 -2
- pyplumio/protocol.py +113 -3
- {pyplumio-0.5.54.dist-info → pyplumio-0.5.56.dist-info}/METADATA +1 -1
- {pyplumio-0.5.54.dist-info → pyplumio-0.5.56.dist-info}/RECORD +8 -8
- {pyplumio-0.5.54.dist-info → pyplumio-0.5.56.dist-info}/WHEEL +0 -0
- {pyplumio-0.5.54.dist-info → pyplumio-0.5.56.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.54.dist-info → pyplumio-0.5.56.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.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
34
|
__commit_id__ = commit_id = None
|
pyplumio/devices/ecomax.py
CHANGED
@@ -139,6 +139,8 @@ REQUIRED: tuple[DataFrameDescription, ...] = (
|
|
139
139
|
|
140
140
|
REQUIRED_TYPES = [description.frame_type for description in REQUIRED]
|
141
141
|
|
142
|
+
SETUP_TIMEOUT: Final = 60.0
|
143
|
+
|
142
144
|
|
143
145
|
class EcoMAX(PhysicalDevice):
|
144
146
|
"""Represents an ecoMAX controller."""
|
@@ -233,10 +235,19 @@ class EcoMAX(PhysicalDevice):
|
|
233
235
|
await super().shutdown()
|
234
236
|
|
235
237
|
@event_listener
|
236
|
-
async def on_event_setup(self, setup: bool) ->
|
238
|
+
async def on_event_setup(self, setup: bool) -> bool:
|
237
239
|
"""Request frames required to set up an ecoMAX entry."""
|
238
240
|
_LOGGER.debug("Setting up device entry")
|
239
|
-
|
241
|
+
|
242
|
+
try:
|
243
|
+
await self.wait_for(ATTR_SENSORS, timeout=SETUP_TIMEOUT)
|
244
|
+
except asyncio.TimeoutError:
|
245
|
+
_LOGGER.error(
|
246
|
+
"Could not setup device entry; no response from device for %u seconds",
|
247
|
+
SETUP_TIMEOUT,
|
248
|
+
)
|
249
|
+
return False
|
250
|
+
|
240
251
|
results = await asyncio.gather(
|
241
252
|
*(
|
242
253
|
self.request(description.provides, description.frame_type)
|
@@ -253,6 +264,7 @@ class EcoMAX(PhysicalDevice):
|
|
253
264
|
self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
|
254
265
|
|
255
266
|
_LOGGER.debug("Device entry setup done")
|
267
|
+
return True
|
256
268
|
|
257
269
|
@event_listener
|
258
270
|
async def on_event_ecomax_parameters(
|
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
|
-
|
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,17 +1,17 @@
|
|
1
1
|
pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
|
2
2
|
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
-
pyplumio/_version.py,sha256=
|
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=
|
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
|
13
13
|
pyplumio/devices/__init__.py,sha256=d0E5hTV7UPa8flq8TNlKf_jt4cOSbRigSE9jjDHrmDI,8302
|
14
|
-
pyplumio/devices/ecomax.py,sha256=
|
14
|
+
pyplumio/devices/ecomax.py,sha256=1QasnLFgNCplSoDXXe5wUr8JQjr6ChSEGijamXtJZVM,16356
|
15
15
|
pyplumio/devices/ecoster.py,sha256=X46ky5XT8jHMFq9sBW0ve8ZI_tjItQDMt4moXsW-ogY,307
|
16
16
|
pyplumio/devices/mixer.py,sha256=7WdUVgwO4VXmaPNzh3ZWpKr2ooRXWemz2KFHAw35_Rk,2731
|
17
17
|
pyplumio/devices/thermostat.py,sha256=MHMKe45fQ7jKlhBVObJ7McbYQKuF6-LOKSHy-9VNsCU,2253
|
@@ -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.
|
60
|
-
pyplumio-0.5.
|
61
|
-
pyplumio-0.5.
|
62
|
-
pyplumio-0.5.
|
63
|
-
pyplumio-0.5.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|