PyPlumIO 0.5.55__py3-none-any.whl → 0.6.0__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/__init__.py +10 -10
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +76 -16
- pyplumio/const.py +1 -3
- pyplumio/filters.py +4 -5
- pyplumio/frames/__init__.py +3 -16
- pyplumio/helpers/async_cache.py +2 -4
- pyplumio/helpers/event_manager.py +1 -3
- pyplumio/parameters/__init__.py +8 -17
- pyplumio/parameters/custom/__init__.py +2 -6
- pyplumio/parameters/ecomax.py +2 -7
- pyplumio/parameters/mixer.py +2 -7
- pyplumio/parameters/thermostat.py +2 -6
- pyplumio/protocol.py +109 -8
- pyplumio/structures/__init__.py +1 -3
- pyplumio/structures/alerts.py +1 -3
- pyplumio/structures/modules.py +1 -4
- pyplumio/structures/network_info.py +3 -3
- pyplumio/structures/product_info.py +1 -3
- pyplumio/structures/program_version.py +1 -4
- pyplumio/structures/schedules.py +3 -21
- pyplumio/utils.py +1 -3
- {pyplumio-0.5.55.dist-info → pyplumio-0.6.0.dist-info}/METADATA +11 -12
- {pyplumio-0.5.55.dist-info → pyplumio-0.6.0.dist-info}/RECORD +27 -27
- {pyplumio-0.5.55.dist-info → pyplumio-0.6.0.dist-info}/WHEEL +0 -0
- {pyplumio-0.5.55.dist-info → pyplumio-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.55.dist-info → pyplumio-0.6.0.dist-info}/top_level.txt +0 -0
pyplumio/__init__.py
CHANGED
@@ -23,17 +23,17 @@ from pyplumio.structures.network_info import EthernetParameters, WirelessParamet
|
|
23
23
|
|
24
24
|
|
25
25
|
def open_serial_connection(
|
26
|
-
|
26
|
+
url: str,
|
27
27
|
baudrate: int = 115200,
|
28
28
|
*,
|
29
29
|
protocol: Protocol | None = None,
|
30
30
|
reconnect_on_failure: bool = True,
|
31
|
-
**
|
31
|
+
**options: Any,
|
32
32
|
) -> SerialConnection:
|
33
33
|
r"""Create a serial connection.
|
34
34
|
|
35
|
-
:param
|
36
|
-
:type
|
35
|
+
:param url: Serial port device url. e. g. /dev/ttyUSB0
|
36
|
+
:type url: str
|
37
37
|
:param baudrate: Serial port baud rate, defaults to 115200
|
38
38
|
:type baudrate: int, optional
|
39
39
|
:param protocol: Protocol that will be used for communication with
|
@@ -42,17 +42,17 @@ def open_serial_connection(
|
|
42
42
|
:param reconnect_on_failure: `True` if PyPlumIO should try
|
43
43
|
reconnecting on failure, otherwise `False`, default to `True`
|
44
44
|
:type reconnect_on_failure: bool, optional
|
45
|
-
:param \**
|
45
|
+
:param \**options: Additional arguments to be passed to
|
46
46
|
serial_asyncio.open_serial_connection()
|
47
47
|
:return: An instance of serial connection
|
48
48
|
:rtype: SerialConnection
|
49
49
|
"""
|
50
50
|
return SerialConnection(
|
51
|
-
|
51
|
+
url,
|
52
52
|
baudrate,
|
53
53
|
protocol=protocol,
|
54
54
|
reconnect_on_failure=reconnect_on_failure,
|
55
|
-
**
|
55
|
+
**options,
|
56
56
|
)
|
57
57
|
|
58
58
|
|
@@ -62,7 +62,7 @@ def open_tcp_connection(
|
|
62
62
|
*,
|
63
63
|
protocol: Protocol | None = None,
|
64
64
|
reconnect_on_failure: bool = True,
|
65
|
-
**
|
65
|
+
**options: Any,
|
66
66
|
) -> TcpConnection:
|
67
67
|
r"""Create a TCP connection.
|
68
68
|
|
@@ -76,7 +76,7 @@ def open_tcp_connection(
|
|
76
76
|
:param reconnect_on_failure: `True` if PyPlumIO should try
|
77
77
|
reconnecting on failure, otherwise `False`, default to `True`
|
78
78
|
:type reconnect_on_failure: bool, optional
|
79
|
-
:param \**
|
79
|
+
:param \**options: Additional arguments to be passed to
|
80
80
|
asyncio.open_connection()
|
81
81
|
:return: An instance of TCP connection
|
82
82
|
:rtype: TcpConnection
|
@@ -86,7 +86,7 @@ def open_tcp_connection(
|
|
86
86
|
port,
|
87
87
|
protocol=protocol,
|
88
88
|
reconnect_on_failure=reconnect_on_failure,
|
89
|
-
**
|
89
|
+
**options,
|
90
90
|
)
|
91
91
|
|
92
92
|
|
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.
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
31
|
+
__version__ = version = '0.6.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
pyplumio/connection.py
CHANGED
@@ -4,11 +4,14 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
|
+
from collections.abc import AsyncGenerator
|
8
|
+
from contextlib import asynccontextmanager
|
7
9
|
import logging
|
8
10
|
from typing import Any, Final
|
9
11
|
|
10
12
|
from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE
|
11
13
|
|
14
|
+
from pyplumio.devices import PhysicalDevice
|
12
15
|
from pyplumio.exceptions import ConnectionFailedError
|
13
16
|
from pyplumio.helpers.task_manager import TaskManager
|
14
17
|
from pyplumio.protocol import AsyncProtocol, Protocol
|
@@ -33,15 +36,17 @@ class Connection(ABC, TaskManager):
|
|
33
36
|
All specific connection classes MUST be inherited from this class.
|
34
37
|
"""
|
35
38
|
|
39
|
+
__slots__ = ("_protocol", "_reconnect_on_failure", "_options")
|
40
|
+
|
36
41
|
_protocol: Protocol
|
37
42
|
_reconnect_on_failure: bool
|
38
|
-
|
43
|
+
_options: dict[str, Any]
|
39
44
|
|
40
45
|
def __init__(
|
41
46
|
self,
|
42
47
|
protocol: Protocol | None = None,
|
43
48
|
reconnect_on_failure: bool = True,
|
44
|
-
**
|
49
|
+
**options: Any,
|
45
50
|
) -> None:
|
46
51
|
"""Initialize a new connection."""
|
47
52
|
super().__init__()
|
@@ -53,7 +58,7 @@ class Connection(ABC, TaskManager):
|
|
53
58
|
|
54
59
|
self._reconnect_on_failure = reconnect_on_failure
|
55
60
|
self._protocol = protocol
|
56
|
-
self.
|
61
|
+
self._options = options
|
57
62
|
|
58
63
|
async def __aenter__(self) -> Connection:
|
59
64
|
"""Provide an entry point for the context manager."""
|
@@ -101,11 +106,62 @@ class Connection(ABC, TaskManager):
|
|
101
106
|
self.cancel_tasks()
|
102
107
|
await self.protocol.shutdown()
|
103
108
|
|
109
|
+
@asynccontextmanager
|
110
|
+
async def device(
|
111
|
+
self, name: str, timeout: float | None = None
|
112
|
+
) -> AsyncGenerator[PhysicalDevice]:
|
113
|
+
"""Get the device in context manager."""
|
114
|
+
if not isinstance(self.protocol, AsyncProtocol):
|
115
|
+
raise NotImplementedError
|
116
|
+
|
117
|
+
yield await self.protocol.get(name, timeout=timeout)
|
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
|
+
|
104
155
|
@property
|
105
156
|
def protocol(self) -> Protocol:
|
106
157
|
"""Return the protocol object."""
|
107
158
|
return self._protocol
|
108
159
|
|
160
|
+
@property
|
161
|
+
def options(self) -> dict[str, Any]:
|
162
|
+
"""Return connection options."""
|
163
|
+
return self._options
|
164
|
+
|
109
165
|
@timeout(CONNECT_TIMEOUT)
|
110
166
|
@abstractmethod
|
111
167
|
async def _open_connection(
|
@@ -117,6 +173,8 @@ class Connection(ABC, TaskManager):
|
|
117
173
|
class TcpConnection(Connection):
|
118
174
|
"""Represents a TCP connection."""
|
119
175
|
|
176
|
+
__slots__ = ("host", "port")
|
177
|
+
|
120
178
|
host: str
|
121
179
|
port: int
|
122
180
|
|
@@ -127,17 +185,17 @@ class TcpConnection(Connection):
|
|
127
185
|
*,
|
128
186
|
protocol: Protocol | None = None,
|
129
187
|
reconnect_on_failure: bool = True,
|
130
|
-
**
|
188
|
+
**options: Any,
|
131
189
|
) -> None:
|
132
190
|
"""Initialize a new TCP connection."""
|
133
|
-
super().__init__(protocol, reconnect_on_failure, **
|
191
|
+
super().__init__(protocol, reconnect_on_failure, **options)
|
134
192
|
self.host = host
|
135
193
|
self.port = port
|
136
194
|
|
137
195
|
def __repr__(self) -> str:
|
138
196
|
"""Return a serializable string representation."""
|
139
197
|
return (
|
140
|
-
f"TcpConnection(host={self.host}, port={self.port},
|
198
|
+
f"TcpConnection(host={self.host}, port={self.port}, options={self.options})"
|
141
199
|
)
|
142
200
|
|
143
201
|
@timeout(CONNECT_TIMEOUT)
|
@@ -146,37 +204,39 @@ class TcpConnection(Connection):
|
|
146
204
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
147
205
|
"""Open the connection and return reader and writer objects."""
|
148
206
|
return await asyncio.open_connection(
|
149
|
-
host=self.host, port=self.port, **self.
|
207
|
+
host=self.host, port=self.port, **self.options
|
150
208
|
)
|
151
209
|
|
152
210
|
|
153
211
|
class SerialConnection(Connection):
|
154
212
|
"""Represents a serial connection."""
|
155
213
|
|
156
|
-
|
214
|
+
__slots__ = ("url", "baudrate")
|
215
|
+
|
216
|
+
url: str
|
157
217
|
baudrate: int
|
158
218
|
|
159
219
|
def __init__(
|
160
220
|
self,
|
161
|
-
|
221
|
+
url: str,
|
162
222
|
baudrate: int = 115200,
|
163
223
|
*,
|
164
224
|
protocol: Protocol | None = None,
|
165
225
|
reconnect_on_failure: bool = True,
|
166
|
-
**
|
226
|
+
**options: Any,
|
167
227
|
) -> None:
|
168
228
|
"""Initialize a new serial connection."""
|
169
|
-
super().__init__(protocol, reconnect_on_failure, **
|
170
|
-
self.
|
229
|
+
super().__init__(protocol, reconnect_on_failure, **options)
|
230
|
+
self.url = url
|
171
231
|
self.baudrate = baudrate
|
172
232
|
|
173
233
|
def __repr__(self) -> str:
|
174
234
|
"""Return a serializable string representation."""
|
175
235
|
return (
|
176
236
|
"SerialConnection("
|
177
|
-
f"
|
237
|
+
f"url={self.url}, "
|
178
238
|
f"baudrate={self.baudrate}, "
|
179
|
-
f"
|
239
|
+
f"options={self.options})"
|
180
240
|
)
|
181
241
|
|
182
242
|
@timeout(CONNECT_TIMEOUT)
|
@@ -185,12 +245,12 @@ class SerialConnection(Connection):
|
|
185
245
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
186
246
|
"""Open the connection and return reader and writer objects."""
|
187
247
|
return await pyserial_asyncio.open_serial_connection(
|
188
|
-
url=self.
|
248
|
+
url=self.url,
|
189
249
|
baudrate=self.baudrate,
|
190
250
|
bytesize=EIGHTBITS,
|
191
251
|
parity=PARITY_NONE,
|
192
252
|
stopbits=STOPBITS_ONE,
|
193
|
-
**self.
|
253
|
+
**self.options,
|
194
254
|
)
|
195
255
|
|
196
256
|
|
pyplumio/const.py
CHANGED
@@ -3,9 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from enum import Enum, IntEnum, unique
|
6
|
-
from typing import Any, Final, Literal
|
7
|
-
|
8
|
-
from typing_extensions import TypeAlias
|
6
|
+
from typing import Any, Final, Literal, TypeAlias
|
9
7
|
|
10
8
|
# General attributes.
|
11
9
|
ATTR_CONNECTED: Final = "connected"
|
pyplumio/filters.py
CHANGED
@@ -15,13 +15,12 @@ from typing import (
|
|
15
15
|
Final,
|
16
16
|
Protocol,
|
17
17
|
SupportsFloat,
|
18
|
+
TypeAlias,
|
18
19
|
TypeVar,
|
19
20
|
overload,
|
20
21
|
runtime_checkable,
|
21
22
|
)
|
22
23
|
|
23
|
-
from typing_extensions import TypeAlias
|
24
|
-
|
25
24
|
from pyplumio.helpers.event_manager import Callback
|
26
25
|
from pyplumio.parameters import Parameter
|
27
26
|
|
@@ -176,7 +175,7 @@ class _Aggregate(Filter):
|
|
176
175
|
|
177
176
|
async def __call__(self, new_value: Any) -> Any:
|
178
177
|
"""Set a new value for the callback."""
|
179
|
-
if not isinstance(new_value,
|
178
|
+
if not isinstance(new_value, float | int | Decimal):
|
180
179
|
raise TypeError(
|
181
180
|
"Aggregate filter can only be used with numeric values, got "
|
182
181
|
f"{type(new_value).__name__}: {new_value}"
|
@@ -187,7 +186,7 @@ class _Aggregate(Filter):
|
|
187
186
|
time_since_call = current_time - self._last_call_time
|
188
187
|
if time_since_call >= self._timeout or len(self._values) >= self._sample_size:
|
189
188
|
sum_of_values = (
|
190
|
-
np.sum(self._values) if numpy_installed else sum(self._values)
|
189
|
+
np.sum(np.array(self._values)) if numpy_installed else sum(self._values)
|
191
190
|
)
|
192
191
|
result = await self._callback(float(sum_of_values))
|
193
192
|
self._last_call_time = current_time
|
@@ -326,7 +325,7 @@ class _Deadband(Filter):
|
|
326
325
|
|
327
326
|
async def __call__(self, new_value: Any) -> Any:
|
328
327
|
"""Set a new value for the callback."""
|
329
|
-
if not isinstance(new_value,
|
328
|
+
if not isinstance(new_value, float | int | Decimal):
|
330
329
|
raise TypeError(
|
331
330
|
"Deadband filter can only be used with numeric values, got "
|
332
331
|
f"{type(new_value).__name__}: {new_value}"
|
pyplumio/frames/__init__.py
CHANGED
@@ -60,12 +60,10 @@ def get_frame_handler(frame_type: int) -> str:
|
|
60
60
|
return f"frames.{module.lower()}s.{type_name}{module.capitalize()}"
|
61
61
|
|
62
62
|
|
63
|
-
@dataclass
|
63
|
+
@dataclass(slots=True)
|
64
64
|
class DataFrameDescription:
|
65
65
|
"""Describes what data is provided by the frame."""
|
66
66
|
|
67
|
-
__slots__ = ("frame_type", "provides")
|
68
|
-
|
69
67
|
frame_type: FrameType
|
70
68
|
provides: str
|
71
69
|
|
@@ -95,6 +93,8 @@ class Frame(ABC):
|
|
95
93
|
_message: bytearray | None
|
96
94
|
_data: dict[str, Any] | None
|
97
95
|
|
96
|
+
__hash__ = object.__hash__
|
97
|
+
|
98
98
|
def __init__(
|
99
99
|
self,
|
100
100
|
recipient: DeviceType = DeviceType.ALL,
|
@@ -114,19 +114,6 @@ class Frame(ABC):
|
|
114
114
|
self._data = data if not kwargs else ensure_dict(data, kwargs)
|
115
115
|
self._message = message
|
116
116
|
|
117
|
-
def __hash__(self) -> int:
|
118
|
-
"""Return a hash of the frame based on its values."""
|
119
|
-
return hash(
|
120
|
-
(
|
121
|
-
self.recipient,
|
122
|
-
self.sender,
|
123
|
-
self.econet_type,
|
124
|
-
self.econet_version,
|
125
|
-
self._message,
|
126
|
-
frozenset(self._data.items()) if self._data else None,
|
127
|
-
)
|
128
|
-
)
|
129
|
-
|
130
117
|
def __eq__(self, other: object) -> bool:
|
131
118
|
"""Compare if this frame is equal to other."""
|
132
119
|
if isinstance(other, Frame):
|
pyplumio/helpers/async_cache.py
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
"""Contains a simple async cache for caching results of async functions."""
|
2
2
|
|
3
|
-
from collections.abc import Awaitable
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
4
|
from functools import wraps
|
5
|
-
from typing import Any,
|
6
|
-
|
7
|
-
from typing_extensions import ParamSpec, TypeAlias
|
5
|
+
from typing import Any, ParamSpec, TypeAlias, TypeVar, cast
|
8
6
|
|
9
7
|
T = TypeVar("T")
|
10
8
|
P = ParamSpec("P")
|
@@ -5,9 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import asyncio
|
6
6
|
from collections.abc import Callable, Coroutine, Generator
|
7
7
|
import inspect
|
8
|
-
from typing import Any, Generic, TypeVar, overload
|
9
|
-
|
10
|
-
from typing_extensions import TypeAlias
|
8
|
+
from typing import Any, Generic, TypeAlias, TypeVar, overload
|
11
9
|
|
12
10
|
from pyplumio.helpers.task_manager import TaskManager
|
13
11
|
|
pyplumio/parameters/__init__.py
CHANGED
@@ -7,10 +7,7 @@ import asyncio
|
|
7
7
|
from contextlib import suppress
|
8
8
|
from dataclasses import asdict, dataclass
|
9
9
|
import logging
|
10
|
-
from typing import TYPE_CHECKING, Any, Literal,
|
11
|
-
|
12
|
-
from dataslots import dataslots
|
13
|
-
from typing_extensions import TypeAlias
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar, get_args
|
14
11
|
|
15
12
|
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
|
16
13
|
from pyplumio.frames import Request
|
@@ -47,19 +44,16 @@ def is_valid_parameter(data: bytearray) -> bool:
|
|
47
44
|
return any(x for x in data if x != BYTE_UNDEFINED)
|
48
45
|
|
49
46
|
|
50
|
-
@dataclass
|
47
|
+
@dataclass(slots=True)
|
51
48
|
class ParameterValues:
|
52
49
|
"""Represents a parameter values."""
|
53
50
|
|
54
|
-
__slots__ = ("value", "min_value", "max_value")
|
55
|
-
|
56
51
|
value: int
|
57
52
|
min_value: int
|
58
53
|
max_value: int
|
59
54
|
|
60
55
|
|
61
|
-
@
|
62
|
-
@dataclass
|
56
|
+
@dataclass(slots=True)
|
63
57
|
class ParameterDescription:
|
64
58
|
"""Represents a parameter description."""
|
65
59
|
|
@@ -67,7 +61,7 @@ class ParameterDescription:
|
|
67
61
|
optimistic: bool = False
|
68
62
|
|
69
63
|
|
70
|
-
NumericType: TypeAlias =
|
64
|
+
NumericType: TypeAlias = int | float
|
71
65
|
|
72
66
|
|
73
67
|
class Parameter(ABC):
|
@@ -128,7 +122,7 @@ class Parameter(ABC):
|
|
128
122
|
handler = getattr(self.values.value, method_to_call)
|
129
123
|
return handler(other.value)
|
130
124
|
|
131
|
-
if isinstance(other,
|
125
|
+
if isinstance(other, int | float | bool) or other in get_args(State):
|
132
126
|
handler = getattr(self.values.value, method_to_call)
|
133
127
|
return handler(self._pack_value(other))
|
134
128
|
else:
|
@@ -317,8 +311,7 @@ class Parameter(ABC):
|
|
317
311
|
"""Create a request to change the parameter."""
|
318
312
|
|
319
313
|
|
320
|
-
@
|
321
|
-
@dataclass
|
314
|
+
@dataclass(slots=True)
|
322
315
|
class NumberDescription(ParameterDescription):
|
323
316
|
"""Represents a parameter description."""
|
324
317
|
|
@@ -395,8 +388,7 @@ class Number(Parameter):
|
|
395
388
|
return self.description.unit_of_measurement
|
396
389
|
|
397
390
|
|
398
|
-
@
|
399
|
-
@dataclass
|
391
|
+
@dataclass(slots=True)
|
400
392
|
class OffsetNumberDescription(NumberDescription):
|
401
393
|
"""Represents a parameter description."""
|
402
394
|
|
@@ -419,8 +411,7 @@ class OffsetNumber(Number):
|
|
419
411
|
return super()._unpack_value(value - self.description.offset)
|
420
412
|
|
421
413
|
|
422
|
-
@
|
423
|
-
@dataclass
|
414
|
+
@dataclass(slots=True)
|
424
415
|
class SwitchDescription(ParameterDescription):
|
425
416
|
"""Represents a switch description."""
|
426
417
|
|
@@ -15,22 +15,18 @@ from pyplumio.utils import to_camelcase
|
|
15
15
|
_LOGGER = logging.getLogger(__name__)
|
16
16
|
|
17
17
|
|
18
|
-
@dataclass
|
18
|
+
@dataclass(slots=True, kw_only=True)
|
19
19
|
class Signature:
|
20
20
|
"""Represents a product signature."""
|
21
21
|
|
22
|
-
__slots__ = ("id", "model")
|
23
|
-
|
24
22
|
id: int
|
25
23
|
model: str
|
26
24
|
|
27
25
|
|
28
|
-
@dataclass
|
26
|
+
@dataclass(slots=True, kw_only=True)
|
29
27
|
class CustomParameter:
|
30
28
|
"""Represents a custom parameter."""
|
31
29
|
|
32
|
-
__slots__ = ("original", "replacement")
|
33
|
-
|
34
30
|
original: str
|
35
31
|
replacement: ParameterDescription
|
36
32
|
|
pyplumio/parameters/ecomax.py
CHANGED
@@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|
6
6
|
from functools import partial
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
|
-
from dataslots import dataslots
|
10
|
-
|
11
9
|
from pyplumio.const import (
|
12
10
|
ATTR_INDEX,
|
13
11
|
ATTR_OFFSET,
|
@@ -78,12 +76,10 @@ class EcomaxParameter(Parameter):
|
|
78
76
|
)
|
79
77
|
|
80
78
|
|
81
|
-
@dataclass
|
79
|
+
@dataclass(slots=True)
|
82
80
|
class EcomaxNumberDescription(EcomaxParameterDescription, OffsetNumberDescription):
|
83
81
|
"""Represents an ecoMAX number description."""
|
84
82
|
|
85
|
-
__slots__ = ()
|
86
|
-
|
87
83
|
|
88
84
|
class EcomaxNumber(EcomaxParameter, OffsetNumber):
|
89
85
|
"""Represents a ecoMAX number."""
|
@@ -93,8 +89,7 @@ class EcomaxNumber(EcomaxParameter, OffsetNumber):
|
|
93
89
|
description: EcomaxNumberDescription
|
94
90
|
|
95
91
|
|
96
|
-
@
|
97
|
-
@dataclass
|
92
|
+
@dataclass(slots=True)
|
98
93
|
class EcomaxSwitchDescription(EcomaxParameterDescription, SwitchDescription):
|
99
94
|
"""Represents an ecoMAX switch description."""
|
100
95
|
|
pyplumio/parameters/mixer.py
CHANGED
@@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|
6
6
|
from functools import cache
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
|
-
from dataslots import dataslots
|
10
|
-
|
11
9
|
from pyplumio.const import (
|
12
10
|
ATTR_DEVICE_INDEX,
|
13
11
|
ATTR_INDEX,
|
@@ -59,12 +57,10 @@ class MixerParameter(Parameter):
|
|
59
57
|
)
|
60
58
|
|
61
59
|
|
62
|
-
@dataclass
|
60
|
+
@dataclass(slots=True)
|
63
61
|
class MixerNumberDescription(MixerParameterDescription, OffsetNumberDescription):
|
64
62
|
"""Represent a mixer number description."""
|
65
63
|
|
66
|
-
__slots__ = ()
|
67
|
-
|
68
64
|
|
69
65
|
class MixerNumber(MixerParameter, OffsetNumber):
|
70
66
|
"""Represents a mixer number."""
|
@@ -74,8 +70,7 @@ class MixerNumber(MixerParameter, OffsetNumber):
|
|
74
70
|
description: MixerNumberDescription
|
75
71
|
|
76
72
|
|
77
|
-
@
|
78
|
-
@dataclass
|
73
|
+
@dataclass(slots=True)
|
79
74
|
class MixerSwitchDescription(MixerParameterDescription, SwitchDescription):
|
80
75
|
"""Represents a mixer switch description."""
|
81
76
|
|
@@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|
6
6
|
from functools import cache
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
|
-
from dataslots import dataslots
|
10
|
-
|
11
9
|
from pyplumio.const import (
|
12
10
|
ATTR_INDEX,
|
13
11
|
ATTR_OFFSET,
|
@@ -77,8 +75,7 @@ class ThermostatParameter(Parameter):
|
|
77
75
|
)
|
78
76
|
|
79
77
|
|
80
|
-
@
|
81
|
-
@dataclass
|
78
|
+
@dataclass(slots=True)
|
82
79
|
class ThermostatNumberDescription(ThermostatParameterDescription, NumberDescription):
|
83
80
|
"""Represent a thermostat number description."""
|
84
81
|
|
@@ -91,8 +88,7 @@ class ThermostatNumber(ThermostatParameter, Number):
|
|
91
88
|
description: ThermostatNumberDescription
|
92
89
|
|
93
90
|
|
94
|
-
@
|
95
|
-
@dataclass
|
91
|
+
@dataclass(slots=True)
|
96
92
|
class ThermostatSwitchDescription(ThermostatParameterDescription, SwitchDescription):
|
97
93
|
"""Represents a thermostat switch description."""
|
98
94
|
|
pyplumio/protocol.py
CHANGED
@@ -5,10 +5,10 @@ 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
|
10
|
-
|
11
|
-
from typing_extensions import TypeAlias
|
11
|
+
from typing import Any, Final, Literal, TypeAlias
|
12
12
|
|
13
13
|
from pyplumio.const import ATTR_CONNECTED, ATTR_SETUP, DeviceType
|
14
14
|
from pyplumio.devices import PhysicalDevice
|
@@ -23,6 +23,7 @@ from pyplumio.structures.network_info import (
|
|
23
23
|
NetworkInfo,
|
24
24
|
WirelessParameters,
|
25
25
|
)
|
26
|
+
from pyplumio.structures.regulator_data import ATTR_REGDATA
|
26
27
|
|
27
28
|
_LOGGER = logging.getLogger(__name__)
|
28
29
|
|
@@ -100,12 +101,10 @@ class DummyProtocol(Protocol):
|
|
100
101
|
await self.close_writer()
|
101
102
|
|
102
103
|
|
103
|
-
@dataclass
|
104
|
+
@dataclass(slots=True)
|
104
105
|
class Queues:
|
105
106
|
"""Represents asyncio queues."""
|
106
107
|
|
107
|
-
__slots__ = ("read", "write")
|
108
|
-
|
109
108
|
read: asyncio.Queue[Frame]
|
110
109
|
write: asyncio.Queue[Frame]
|
111
110
|
|
@@ -114,6 +113,84 @@ class Queues:
|
|
114
113
|
await asyncio.gather(self.read.join(), self.write.join())
|
115
114
|
|
116
115
|
|
116
|
+
NEVER: Final = "never"
|
117
|
+
|
118
|
+
|
119
|
+
@dataclass(slots=True, kw_only=True)
|
120
|
+
class Statistics:
|
121
|
+
"""Represents a connection statistics."""
|
122
|
+
|
123
|
+
#: Number of received bytes. Resets on reconnect.
|
124
|
+
received_bytes: int = 0
|
125
|
+
|
126
|
+
#: Number of received frames. Resets on reconnect.
|
127
|
+
received_frames: int = 0
|
128
|
+
|
129
|
+
#: Number of sent bytes. Resets on reconnect.
|
130
|
+
sent_bytes: int = 0
|
131
|
+
|
132
|
+
#: Number of sent frames. Resets on reconnect.
|
133
|
+
sent_frames: int = 0
|
134
|
+
|
135
|
+
#: Number of failed frames. Resets on reconnect.
|
136
|
+
failed_frames: int = 0
|
137
|
+
|
138
|
+
#: Datetime object representing connection time
|
139
|
+
connected_since: datetime | Literal["never"] = NEVER
|
140
|
+
|
141
|
+
#: Datetime object representing last connection loss event
|
142
|
+
connection_loss_at: datetime | Literal["never"] = NEVER
|
143
|
+
|
144
|
+
#: Number of connection lost event
|
145
|
+
connection_losses: int = 0
|
146
|
+
|
147
|
+
#: List of statistics for connected devices
|
148
|
+
devices: list[DeviceStatistics] = field(default_factory=list)
|
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
|
157
|
+
|
158
|
+
if received:
|
159
|
+
self.received_bytes += received.length
|
160
|
+
self.received_frames += 1
|
161
|
+
|
162
|
+
def track_connection_loss(self) -> None:
|
163
|
+
"""Increase connection loss counter and store the datetime."""
|
164
|
+
self.connection_losses += 1
|
165
|
+
self.connection_loss_at = datetime.now()
|
166
|
+
|
167
|
+
def reset_transfer_statistics(self) -> None:
|
168
|
+
"""Reset transfer statistics."""
|
169
|
+
self.sent_bytes = 0
|
170
|
+
self.sent_frames = 0
|
171
|
+
self.received_bytes = 0
|
172
|
+
self.received_frames = 0
|
173
|
+
self.failed_frames = 0
|
174
|
+
|
175
|
+
|
176
|
+
@dataclass(slots=True)
|
177
|
+
class DeviceStatistics:
|
178
|
+
"""Represents a device statistics."""
|
179
|
+
|
180
|
+
#: Device name
|
181
|
+
name: str
|
182
|
+
|
183
|
+
#: Datetime object representing connection time
|
184
|
+
connected_since: datetime | Literal["never"] = NEVER
|
185
|
+
|
186
|
+
#: Datetime object representing time when device was last seen
|
187
|
+
last_seen: datetime | Literal["never"] = NEVER
|
188
|
+
|
189
|
+
async def update_last_seen(self, _: Any) -> None:
|
190
|
+
"""Update last seen property."""
|
191
|
+
self.last_seen = datetime.now()
|
192
|
+
|
193
|
+
|
117
194
|
class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
118
195
|
"""Represents an async protocol.
|
119
196
|
|
@@ -134,6 +211,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
134
211
|
_network: NetworkInfo
|
135
212
|
_queues: Queues
|
136
213
|
_entry_lock: asyncio.Lock
|
214
|
+
_statistics: Statistics
|
137
215
|
|
138
216
|
def __init__(
|
139
217
|
self,
|
@@ -150,6 +228,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
150
228
|
)
|
151
229
|
self._queues = Queues(read=asyncio.Queue(), write=asyncio.Queue())
|
152
230
|
self._entry_lock = asyncio.Lock()
|
231
|
+
self._statistics = Statistics()
|
153
232
|
|
154
233
|
def connection_established(
|
155
234
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
@@ -172,6 +251,8 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
172
251
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
173
252
|
|
174
253
|
self.connected.set()
|
254
|
+
self.statistics.reset_transfer_statistics()
|
255
|
+
self.statistics.connected_since = datetime.now()
|
175
256
|
|
176
257
|
async def _connection_close(self) -> None:
|
177
258
|
"""Close the connection if it is established."""
|
@@ -200,19 +281,26 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
200
281
|
self, queues: Queues, reader: FrameReader, writer: FrameWriter
|
201
282
|
) -> None:
|
202
283
|
"""Handle frame reads and writes."""
|
284
|
+
statistics = self.statistics
|
203
285
|
await self.connected.wait()
|
204
286
|
while self.connected.is_set():
|
205
287
|
try:
|
288
|
+
request = None
|
206
289
|
if not queues.write.empty():
|
207
|
-
|
290
|
+
request = await queues.write.get()
|
291
|
+
await writer.write(request)
|
208
292
|
queues.write.task_done()
|
209
293
|
|
210
294
|
if response := await reader.read():
|
211
295
|
queues.read.put_nowait(response)
|
212
296
|
|
297
|
+
statistics.update_transfer_statistics(request, response)
|
298
|
+
|
213
299
|
except ProtocolError as e:
|
300
|
+
statistics.failed_frames += 1
|
214
301
|
_LOGGER.debug("Can't process received frame: %s", e)
|
215
302
|
except (OSError, asyncio.TimeoutError):
|
303
|
+
statistics.track_connection_loss()
|
216
304
|
self.create_task(self.connection_lost())
|
217
305
|
break
|
218
306
|
except Exception:
|
@@ -239,8 +327,21 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
239
327
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
240
328
|
device.dispatch_nowait(ATTR_SETUP, True)
|
241
329
|
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)
|
242
338
|
|
243
339
|
return self.data[name]
|
244
340
|
|
341
|
+
@property
|
342
|
+
def statistics(self) -> Statistics:
|
343
|
+
"""Return the statistics."""
|
344
|
+
return self._statistics
|
345
|
+
|
245
346
|
|
246
|
-
__all__ = ["Protocol", "DummyProtocol", "AsyncProtocol"]
|
347
|
+
__all__ = ["Protocol", "DummyProtocol", "AsyncProtocol", "Statistics"]
|
pyplumio/structures/__init__.py
CHANGED
pyplumio/structures/alerts.py
CHANGED
@@ -56,12 +56,10 @@ def seconds_to_datetime(timestamp: int) -> datetime:
|
|
56
56
|
return datetime(**dict(datetime_kwargs(timestamp)))
|
57
57
|
|
58
58
|
|
59
|
-
@dataclass
|
59
|
+
@dataclass(slots=True)
|
60
60
|
class Alert:
|
61
61
|
"""Represents a device alert."""
|
62
62
|
|
63
|
-
__slots__ = ("code", "from_dt", "to_dt")
|
64
|
-
|
65
63
|
code: int
|
66
64
|
from_dt: datetime
|
67
65
|
to_dt: datetime | None
|
pyplumio/structures/modules.py
CHANGED
@@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|
6
6
|
import struct
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
|
-
from dataslots import dataslots
|
10
|
-
|
11
9
|
from pyplumio.const import BYTE_UNDEFINED
|
12
10
|
from pyplumio.structures import StructureDecoder
|
13
11
|
from pyplumio.utils import ensure_dict
|
@@ -32,8 +30,7 @@ struct_version = struct.Struct("<BBB")
|
|
32
30
|
struct_vendor = struct.Struct("<BB")
|
33
31
|
|
34
32
|
|
35
|
-
@
|
36
|
-
@dataclass
|
33
|
+
@dataclass(slots=True)
|
37
34
|
class ConnectedModules:
|
38
35
|
"""Represents a firmware version info for connected module."""
|
39
36
|
|
@@ -18,7 +18,7 @@ DEFAULT_NETMASK: Final = "255.255.255.0"
|
|
18
18
|
NETWORK_INFO_SIZE: Final = 25
|
19
19
|
|
20
20
|
|
21
|
-
@dataclass(frozen=True)
|
21
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
22
22
|
class EthernetParameters:
|
23
23
|
"""Represents an ethernet parameters."""
|
24
24
|
|
@@ -35,7 +35,7 @@ class EthernetParameters:
|
|
35
35
|
status: bool = True
|
36
36
|
|
37
37
|
|
38
|
-
@dataclass(frozen=True)
|
38
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
39
39
|
class WirelessParameters(EthernetParameters):
|
40
40
|
"""Represents a wireless network parameters."""
|
41
41
|
|
@@ -50,7 +50,7 @@ class WirelessParameters(EthernetParameters):
|
|
50
50
|
signal_quality: int = 100
|
51
51
|
|
52
52
|
|
53
|
-
@dataclass(frozen=True)
|
53
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
54
54
|
class NetworkInfo:
|
55
55
|
"""Represents a network parameters."""
|
56
56
|
|
@@ -63,12 +63,10 @@ def format_model_name(model_name: str) -> str:
|
|
63
63
|
return model_name
|
64
64
|
|
65
65
|
|
66
|
-
@dataclass(frozen=True)
|
66
|
+
@dataclass(frozen=True, slots=True)
|
67
67
|
class ProductInfo:
|
68
68
|
"""Represents a product info provided by an UID response."""
|
69
69
|
|
70
|
-
__slots__ = ("type", "id", "uid", "logo", "image", "model")
|
71
|
-
|
72
70
|
type: ProductType
|
73
71
|
id: int
|
74
72
|
uid: str
|
@@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|
6
6
|
import struct
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
|
-
from dataslots import dataslots
|
10
|
-
|
11
9
|
from pyplumio._version import __version_tuple__
|
12
10
|
from pyplumio.structures import Structure
|
13
11
|
from pyplumio.utils import ensure_dict
|
@@ -21,8 +19,7 @@ SOFTWARE_VERSION: Final = ".".join(str(x) for x in __version_tuple__[0:3])
|
|
21
19
|
struct_program_version = struct.Struct("<2sB2s3s3HB")
|
22
20
|
|
23
21
|
|
24
|
-
@
|
25
|
-
@dataclass
|
22
|
+
@dataclass(slots=True)
|
26
23
|
class VersionInfo:
|
27
24
|
"""Represents a version info provided in program version response."""
|
28
25
|
|
pyplumio/structures/schedules.py
CHANGED
@@ -8,8 +8,6 @@ import datetime as dt
|
|
8
8
|
from functools import lru_cache, reduce
|
9
9
|
from typing import Annotated, Any, Final, get_args
|
10
10
|
|
11
|
-
from dataslots import dataslots
|
12
|
-
|
13
11
|
from pyplumio.const import (
|
14
12
|
ATTR_PARAMETER,
|
15
13
|
ATTR_SCHEDULE,
|
@@ -91,8 +89,6 @@ SCHEDULES: tuple[str, ...] = (
|
|
91
89
|
class ScheduleParameterDescription(ParameterDescription):
|
92
90
|
"""Represent a schedule parameter description."""
|
93
91
|
|
94
|
-
__slots__ = ()
|
95
|
-
|
96
92
|
|
97
93
|
class ScheduleParameter(Parameter):
|
98
94
|
"""Represents a schedule parameter."""
|
@@ -112,8 +108,7 @@ class ScheduleParameter(Parameter):
|
|
112
108
|
)
|
113
109
|
|
114
110
|
|
115
|
-
@
|
116
|
-
@dataclass
|
111
|
+
@dataclass(slots=True)
|
117
112
|
class ScheduleNumberDescription(ScheduleParameterDescription, NumberDescription):
|
118
113
|
"""Represents a schedule number description."""
|
119
114
|
|
@@ -126,8 +121,7 @@ class ScheduleNumber(ScheduleParameter, Number):
|
|
126
121
|
description: ScheduleNumberDescription
|
127
122
|
|
128
123
|
|
129
|
-
@
|
130
|
-
@dataclass
|
124
|
+
@dataclass(slots=True)
|
131
125
|
class ScheduleSwitchDescription(ScheduleParameterDescription, SwitchDescription):
|
132
126
|
"""Represents a schedule switch description."""
|
133
127
|
|
@@ -273,22 +267,10 @@ class ScheduleDay(MutableMapping):
|
|
273
267
|
return cls({get_time(index): state for index, state in enumerate(intervals)})
|
274
268
|
|
275
269
|
|
276
|
-
@dataclass
|
270
|
+
@dataclass(slots=True)
|
277
271
|
class Schedule(Iterable):
|
278
272
|
"""Represents a weekly schedule."""
|
279
273
|
|
280
|
-
__slots__ = (
|
281
|
-
"name",
|
282
|
-
"device",
|
283
|
-
"sunday",
|
284
|
-
"monday",
|
285
|
-
"tuesday",
|
286
|
-
"wednesday",
|
287
|
-
"thursday",
|
288
|
-
"friday",
|
289
|
-
"saturday",
|
290
|
-
)
|
291
|
-
|
292
274
|
name: str
|
293
275
|
device: PhysicalDevice
|
294
276
|
|
pyplumio/utils.py
CHANGED
@@ -5,9 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import asyncio
|
6
6
|
from collections.abc import Awaitable, Callable, Mapping
|
7
7
|
from functools import wraps
|
8
|
-
from typing import TypeVar
|
9
|
-
|
10
|
-
from typing_extensions import ParamSpec
|
8
|
+
from typing import ParamSpec, TypeVar
|
11
9
|
|
12
10
|
KT = TypeVar("KT") # Key type.
|
13
11
|
VT = TypeVar("VT") # Value type.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
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
|
@@ -13,30 +13,27 @@ Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
15
15
|
Classifier: Operating System :: OS Independent
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
21
20
|
Classifier: Topic :: Software Development :: Libraries
|
22
21
|
Classifier: Topic :: Home Automation
|
23
|
-
Requires-Python: >=3.
|
22
|
+
Requires-Python: >=3.10
|
24
23
|
Description-Content-Type: text/markdown
|
25
24
|
License-File: LICENSE
|
26
|
-
Requires-Dist: dataslots==1.2.0
|
27
25
|
Requires-Dist: pyserial-asyncio==0.6
|
28
|
-
Requires-Dist: typing-extensions<5.0,>=4.14.0
|
29
26
|
Provides-Extra: test
|
30
27
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
-
Requires-Dist: coverage==7.10.
|
28
|
+
Requires-Dist: coverage==7.10.6; extra == "test"
|
32
29
|
Requires-Dist: freezegun==1.5.5; extra == "test"
|
33
|
-
Requires-Dist: mypy==1.
|
30
|
+
Requires-Dist: mypy==1.18.1; extra == "test"
|
34
31
|
Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
35
32
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
|
-
Requires-Dist: pytest==8.4.
|
33
|
+
Requires-Dist: pytest==8.4.2; extra == "test"
|
37
34
|
Requires-Dist: pytest-asyncio==1.1.0; extra == "test"
|
38
|
-
Requires-Dist: ruff==0.
|
39
|
-
Requires-Dist: tox==4.
|
35
|
+
Requires-Dist: ruff==0.13.0; extra == "test"
|
36
|
+
Requires-Dist: tox==4.30.2; extra == "test"
|
40
37
|
Requires-Dist: types-pyserial==3.5.0.20250822; extra == "test"
|
41
38
|
Provides-Extra: docs
|
42
39
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
@@ -53,8 +50,8 @@ Dynamic: license-file
|
|
53
50
|
[](https://badge.fury.io/py/PyPlumIO)
|
54
51
|
[](https://pypi.python.org/pypi/pyplumio/)
|
55
52
|
[](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
|
56
|
-
[](https://qlty.sh/gh/denpamusic/projects/PyPlumIO)
|
54
|
+
[](https://qlty.sh/gh/denpamusic/projects/PyPlumIO)
|
58
55
|
[](https://guidelines.denpa.pro/stability#release-candidate)
|
59
56
|
[](https://github.com/astral-sh/ruff)
|
60
57
|
|
@@ -82,11 +79,13 @@ through network by using RS-485 to Ethernet/WiFi converter.
|
|
82
79
|
- [Callbacks](https://pyplumio.denpa.pro/callbacks.html)
|
83
80
|
- [Mixers/Thermostats](https://pyplumio.denpa.pro/mixers_thermostats.html)
|
84
81
|
- [Schedules](https://pyplumio.denpa.pro/schedules.html)
|
82
|
+
- [Statistics](https://pyplumio.denpa.pro/statistics.html)
|
85
83
|
- [Protocol](https://pyplumio.denpa.pro/protocol.html)
|
86
84
|
- [Frame Structure](https://pyplumio.denpa.pro/protocol.html#frame-structure)
|
87
85
|
- [Requests and Responses](https://pyplumio.denpa.pro/protocol.html#requests-and-responses)
|
88
86
|
- [Communication](https://pyplumio.denpa.pro/protocol.html#communication)
|
89
87
|
- [Versioning](https://pyplumio.denpa.pro/protocol.html#versioning)
|
88
|
+
- [Supported frames](https://pyplumio.denpa.pro/frames.html)
|
90
89
|
|
91
90
|
## Quickstart
|
92
91
|
|
@@ -1,37 +1,37 @@
|
|
1
|
-
pyplumio/__init__.py,sha256=
|
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=
|
5
|
-
pyplumio/const.py,sha256=
|
3
|
+
pyplumio/_version.py,sha256=MAYWefOLb6kbIRub18WSzK6ggSjz1LNLy9aDRlX9Ea4,704
|
4
|
+
pyplumio/connection.py,sha256=9Gzg6FXMU-HjspsDnm9XH8ZPBO29AZ6dKS2-eg8P8Z0,7686
|
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
|
-
pyplumio/filters.py,sha256=
|
9
|
-
pyplumio/protocol.py,sha256=
|
8
|
+
pyplumio/filters.py,sha256=sBEnr0i_1XMbIwIEA24npbpe5yevSRneynlsqJMyfko,15642
|
9
|
+
pyplumio/protocol.py,sha256=jGx5b8y_1jbdFbjL_ZbUjpDvgBYhn5JUBsVsf_De6Ls,11614
|
10
10
|
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
pyplumio/stream.py,sha256=zFMKZ_GxsSGcaBTJigVM1CK3uGjlEJXgcvKqus8MDzk,7740
|
12
|
-
pyplumio/utils.py,sha256=
|
12
|
+
pyplumio/utils.py,sha256=SV47Y6QC_KL-gPmk6KQgx7ArExzNHGKuaddGAHjT9rs,1839
|
13
13
|
pyplumio/devices/__init__.py,sha256=d0E5hTV7UPa8flq8TNlKf_jt4cOSbRigSE9jjDHrmDI,8302
|
14
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
|
18
|
-
pyplumio/frames/__init__.py,sha256=
|
18
|
+
pyplumio/frames/__init__.py,sha256=jIYP31yP60FVXp8ygOcKkbJCosodiqWCvnrY9FOgH4g,7885
|
19
19
|
pyplumio/frames/messages.py,sha256=ImQGWFFTa2eaXfytQmFZKC-IxyPRkxD8qp0bEm16-ws,3628
|
20
20
|
pyplumio/frames/requests.py,sha256=jr-_XSSCCDDTbAmrw95CKyWa5nb7JNeGzZ2jDXIxlAo,7348
|
21
21
|
pyplumio/frames/responses.py,sha256=M6Ky4gg2AoShmRXX0x6nftajxrvmQLKPVRWbwyhvI0E,6663
|
22
22
|
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
23
|
-
pyplumio/helpers/async_cache.py,sha256=
|
24
|
-
pyplumio/helpers/event_manager.py,sha256=
|
23
|
+
pyplumio/helpers/async_cache.py,sha256=PUkUTo3lmIhslejg0dGWjbcES09E62d9YYgDcBK_G6Q,1275
|
24
|
+
pyplumio/helpers/event_manager.py,sha256=cev3_X5a7rBvT4KXIwGpyAnOdWd-3svWDETN3yumkhg,8067
|
25
25
|
pyplumio/helpers/factory.py,sha256=c3sitnkUjJWz7fPpTE9uRIpa8h46Qim3xsAblMw3eDo,1049
|
26
26
|
pyplumio/helpers/task_manager.py,sha256=N71F6Ag1HHxdf5zJeCMcEziFdH9lmJKtMPoRGjJ-E40,1209
|
27
|
-
pyplumio/parameters/__init__.py,sha256=
|
28
|
-
pyplumio/parameters/ecomax.py,sha256=
|
29
|
-
pyplumio/parameters/mixer.py,sha256=
|
30
|
-
pyplumio/parameters/thermostat.py,sha256
|
31
|
-
pyplumio/parameters/custom/__init__.py,sha256=
|
27
|
+
pyplumio/parameters/__init__.py,sha256=d2gLX-Ve6UxwxLbiTQU6AGNYrC4ywXK1dJ7F92OkfgM,16108
|
28
|
+
pyplumio/parameters/ecomax.py,sha256=KjHlkVZK2XYEl4HNSdCRLAnv0KEn7gjnEO_CsKFZwIw,26199
|
29
|
+
pyplumio/parameters/mixer.py,sha256=cjwe6AJdboAIEnCeiYNqIRmOVo3dSQqbMTWgiCSx8J8,6606
|
30
|
+
pyplumio/parameters/thermostat.py,sha256=sRAndI87jANM8uvdQc1LdkT6_baDxf0AEAFVYRstzNE,5039
|
31
|
+
pyplumio/parameters/custom/__init__.py,sha256=EeddoseRsh2Gxche3e3woRBgNszraOnLUs9TciK7dCA,3168
|
32
32
|
pyplumio/parameters/custom/ecomax_860d3_hb.py,sha256=IsNgDXmV90QpBilDV4fGSBtIUEQJJbR9rjnfCr3-pHE,2840
|
33
|
-
pyplumio/structures/__init__.py,sha256=
|
34
|
-
pyplumio/structures/alerts.py,sha256=
|
33
|
+
pyplumio/structures/__init__.py,sha256=tb62y-x466WSogdjNpsvqcD3Kiz7xMW604m2-yJH3jc,1329
|
34
|
+
pyplumio/structures/alerts.py,sha256=9cBzxo1R5erJVeQUdWOmEDG82wXgm7vAU9X6xJjRAjk,3690
|
35
35
|
pyplumio/structures/boiler_load.py,sha256=e-6itp9L6iJeeOyhSTiOclHLuYmqG7KkcepsHwJSQSI,894
|
36
36
|
pyplumio/structures/boiler_power.py,sha256=7CdOk-pYLEpy06oRBAeichvq8o-a2RcesB0tzo9ccBs,951
|
37
37
|
pyplumio/structures/ecomax_parameters.py,sha256=E_s5bO0RqX8p1rM5DtYAsEXcHqS8P6Tg4AGm21cxsnM,1663
|
@@ -42,22 +42,22 @@ pyplumio/structures/fuel_level.py,sha256=-zUKApVJaZZzc1q52vqO3K2Mya43c6vRgw45d2x
|
|
42
42
|
pyplumio/structures/lambda_sensor.py,sha256=09nM4Hwn1X275LzFpDihtpzkazwgJXAbx4NFqUkhbNM,1609
|
43
43
|
pyplumio/structures/mixer_parameters.py,sha256=JMSySqI7TUGKdFtDp1P5DJm5EAbijMSz-orRrAe1KlQ,2041
|
44
44
|
pyplumio/structures/mixer_sensors.py,sha256=ChgLhC3p4fyoPy1EKe0BQTvXOPZEISbcK2HyamrNaN8,2450
|
45
|
-
pyplumio/structures/modules.py,sha256
|
46
|
-
pyplumio/structures/network_info.py,sha256=
|
45
|
+
pyplumio/structures/modules.py,sha256=-8krDmCtrLwP3GvVMe3e-dN8Zbe4R0F1cZZuEo6N2zc,2759
|
46
|
+
pyplumio/structures/network_info.py,sha256=g5SkVS8QhUKa-Pt8rxJ5BAbz09hck6-npZb5a8KjIEg,4405
|
47
47
|
pyplumio/structures/output_flags.py,sha256=upVIgAH2JNncHFXvjE-t6oTFF-JNwwZbyGjfrcKWtz0,1508
|
48
48
|
pyplumio/structures/outputs.py,sha256=3NP5lArzQiihRC4QzBuWAHL9hhjvGxNkKmeoYZnDD-0,2291
|
49
49
|
pyplumio/structures/pending_alerts.py,sha256=b1uMmDHTGv8eE0h1vGBrKsPxlwBmUad7HgChnDDLK_g,801
|
50
|
-
pyplumio/structures/product_info.py,sha256=
|
51
|
-
pyplumio/structures/program_version.py,sha256=
|
50
|
+
pyplumio/structures/product_info.py,sha256=QEr2x8GAoXCc1_UzdaVWALRdi2ouMrBtGb684OBNgAQ,3255
|
51
|
+
pyplumio/structures/program_version.py,sha256=QLe_jFZcUOjWsEXrnXRiueFb4MR0coIGOymTtBiYtyg,2589
|
52
52
|
pyplumio/structures/regulator_data.py,sha256=SYKI1YPC3mDAth-SpYejttbD0IzBfobjgC-uy_uUKnw,2333
|
53
53
|
pyplumio/structures/regulator_data_schema.py,sha256=0SapbZCGzqAHmHC7dwhufszJ9FNo_ZO_XMrFGNiUe-w,1547
|
54
|
-
pyplumio/structures/schedules.py,sha256=
|
54
|
+
pyplumio/structures/schedules.py,sha256=IJ7hxGmLxgCtzGrMncXJBRddbc4quf91Wv_R-Y3ZJXA,11820
|
55
55
|
pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8pLI,1325
|
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.
|
60
|
-
pyplumio-0.
|
61
|
-
pyplumio-0.
|
62
|
-
pyplumio-0.
|
63
|
-
pyplumio-0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|