PyPlumIO 0.5.56__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/__init__.py +10 -10
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +40 -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 +80 -79
- 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.56.dist-info → pyplumio-0.6.1.dist-info}/METADATA +12 -14
- {pyplumio-0.5.56.dist-info → pyplumio-0.6.1.dist-info}/RECORD +27 -27
- {pyplumio-0.5.56.dist-info → pyplumio-0.6.1.dist-info}/WHEEL +0 -0
- {pyplumio-0.5.56.dist-info → pyplumio-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.56.dist-info → pyplumio-0.6.1.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.1'
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 1)
|
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,26 @@ 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
|
+
|
104
119
|
@property
|
105
120
|
def protocol(self) -> Protocol:
|
106
121
|
"""Return the protocol object."""
|
107
122
|
return self._protocol
|
108
123
|
|
124
|
+
@property
|
125
|
+
def options(self) -> dict[str, Any]:
|
126
|
+
"""Return connection options."""
|
127
|
+
return self._options
|
128
|
+
|
109
129
|
@timeout(CONNECT_TIMEOUT)
|
110
130
|
@abstractmethod
|
111
131
|
async def _open_connection(
|
@@ -117,6 +137,8 @@ class Connection(ABC, TaskManager):
|
|
117
137
|
class TcpConnection(Connection):
|
118
138
|
"""Represents a TCP connection."""
|
119
139
|
|
140
|
+
__slots__ = ("host", "port")
|
141
|
+
|
120
142
|
host: str
|
121
143
|
port: int
|
122
144
|
|
@@ -127,17 +149,17 @@ class TcpConnection(Connection):
|
|
127
149
|
*,
|
128
150
|
protocol: Protocol | None = None,
|
129
151
|
reconnect_on_failure: bool = True,
|
130
|
-
**
|
152
|
+
**options: Any,
|
131
153
|
) -> None:
|
132
154
|
"""Initialize a new TCP connection."""
|
133
|
-
super().__init__(protocol, reconnect_on_failure, **
|
155
|
+
super().__init__(protocol, reconnect_on_failure, **options)
|
134
156
|
self.host = host
|
135
157
|
self.port = port
|
136
158
|
|
137
159
|
def __repr__(self) -> str:
|
138
160
|
"""Return a serializable string representation."""
|
139
161
|
return (
|
140
|
-
f"TcpConnection(host={self.host}, port={self.port},
|
162
|
+
f"TcpConnection(host={self.host}, port={self.port}, options={self.options})"
|
141
163
|
)
|
142
164
|
|
143
165
|
@timeout(CONNECT_TIMEOUT)
|
@@ -146,37 +168,39 @@ class TcpConnection(Connection):
|
|
146
168
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
147
169
|
"""Open the connection and return reader and writer objects."""
|
148
170
|
return await asyncio.open_connection(
|
149
|
-
host=self.host, port=self.port, **self.
|
171
|
+
host=self.host, port=self.port, **self.options
|
150
172
|
)
|
151
173
|
|
152
174
|
|
153
175
|
class SerialConnection(Connection):
|
154
176
|
"""Represents a serial connection."""
|
155
177
|
|
156
|
-
|
178
|
+
__slots__ = ("url", "baudrate")
|
179
|
+
|
180
|
+
url: str
|
157
181
|
baudrate: int
|
158
182
|
|
159
183
|
def __init__(
|
160
184
|
self,
|
161
|
-
|
185
|
+
url: str,
|
162
186
|
baudrate: int = 115200,
|
163
187
|
*,
|
164
188
|
protocol: Protocol | None = None,
|
165
189
|
reconnect_on_failure: bool = True,
|
166
|
-
**
|
190
|
+
**options: Any,
|
167
191
|
) -> None:
|
168
192
|
"""Initialize a new serial connection."""
|
169
|
-
super().__init__(protocol, reconnect_on_failure, **
|
170
|
-
self.
|
193
|
+
super().__init__(protocol, reconnect_on_failure, **options)
|
194
|
+
self.url = url
|
171
195
|
self.baudrate = baudrate
|
172
196
|
|
173
197
|
def __repr__(self) -> str:
|
174
198
|
"""Return a serializable string representation."""
|
175
199
|
return (
|
176
200
|
"SerialConnection("
|
177
|
-
f"
|
201
|
+
f"url={self.url}, "
|
178
202
|
f"baudrate={self.baudrate}, "
|
179
|
-
f"
|
203
|
+
f"options={self.options})"
|
180
204
|
)
|
181
205
|
|
182
206
|
@timeout(CONNECT_TIMEOUT)
|
@@ -185,12 +209,12 @@ class SerialConnection(Connection):
|
|
185
209
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
186
210
|
"""Open the connection and return reader and writer objects."""
|
187
211
|
return await pyserial_asyncio.open_serial_connection(
|
188
|
-
url=self.
|
212
|
+
url=self.url,
|
189
213
|
baudrate=self.baudrate,
|
190
214
|
bytesize=EIGHTBITS,
|
191
215
|
parity=PARITY_NONE,
|
192
216
|
stopbits=STOPBITS_ONE,
|
193
|
-
**self.
|
217
|
+
**self.options,
|
194
218
|
)
|
195
219
|
|
196
220
|
|
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
@@ -8,10 +8,7 @@ from collections.abc import Awaitable, Callable
|
|
8
8
|
from dataclasses import dataclass, field
|
9
9
|
from datetime import datetime
|
10
10
|
import logging
|
11
|
-
from typing import Any, Final, Literal
|
12
|
-
|
13
|
-
from dataslots import dataslots
|
14
|
-
from typing_extensions import TypeAlias
|
11
|
+
from typing import Any, Final, Literal, TypeAlias
|
15
12
|
|
16
13
|
from pyplumio.const import ATTR_CONNECTED, ATTR_SETUP, DeviceType
|
17
14
|
from pyplumio.devices import PhysicalDevice
|
@@ -104,12 +101,10 @@ class DummyProtocol(Protocol):
|
|
104
101
|
await self.close_writer()
|
105
102
|
|
106
103
|
|
107
|
-
@dataclass
|
104
|
+
@dataclass(slots=True)
|
108
105
|
class Queues:
|
109
106
|
"""Represents asyncio queues."""
|
110
107
|
|
111
|
-
__slots__ = ("read", "write")
|
112
|
-
|
113
108
|
read: asyncio.Queue[Frame]
|
114
109
|
write: asyncio.Queue[Frame]
|
115
110
|
|
@@ -121,52 +116,57 @@ class Queues:
|
|
121
116
|
NEVER: Final = "never"
|
122
117
|
|
123
118
|
|
124
|
-
@
|
125
|
-
@dataclass
|
119
|
+
@dataclass(slots=True, kw_only=True)
|
126
120
|
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
|
-
"""
|
121
|
+
"""Represents a connection statistics."""
|
148
122
|
|
123
|
+
#: Number of received bytes. Resets on reconnect.
|
149
124
|
received_bytes: int = 0
|
125
|
+
|
126
|
+
#: Number of received frames. Resets on reconnect.
|
150
127
|
received_frames: int = 0
|
128
|
+
|
129
|
+
#: Number of sent bytes. Resets on reconnect.
|
151
130
|
sent_bytes: int = 0
|
131
|
+
|
132
|
+
#: Number of sent frames. Resets on reconnect.
|
152
133
|
sent_frames: int = 0
|
134
|
+
|
135
|
+
#: Number of failed frames. Resets on reconnect.
|
153
136
|
failed_frames: int = 0
|
137
|
+
|
138
|
+
#: Datetime object representing connection time
|
154
139
|
connected_since: datetime | Literal["never"] = NEVER
|
140
|
+
|
141
|
+
#: Datetime object representing last connection loss event
|
155
142
|
connection_loss_at: datetime | Literal["never"] = NEVER
|
143
|
+
|
144
|
+
#: Number of connection lost event
|
156
145
|
connection_losses: int = 0
|
157
|
-
devices: list[DeviceStatistics] = field(default_factory=list)
|
158
146
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
147
|
+
#: List of statistics for connected devices
|
148
|
+
devices: set[DeviceStatistics] = field(default_factory=set)
|
149
|
+
|
150
|
+
def update_sent(self, frame: Frame) -> None:
|
151
|
+
"""Update sent frames statistics."""
|
152
|
+
self.sent_bytes += frame.length
|
153
|
+
self.sent_frames += 1
|
154
|
+
|
155
|
+
def update_received(self, frame: Frame) -> None:
|
156
|
+
"""Update received frames statistics."""
|
157
|
+
self.received_bytes += frame.length
|
158
|
+
self.received_frames += 1
|
166
159
|
|
167
|
-
|
168
|
-
|
169
|
-
|
160
|
+
def update_connection_lost(self) -> None:
|
161
|
+
"""Update connection lost counter."""
|
162
|
+
self.connection_losses += 1
|
163
|
+
self.connection_loss_at = datetime.now()
|
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
170
|
|
171
171
|
def reset_transfer_statistics(self) -> None:
|
172
172
|
"""Reset transfer statistics."""
|
@@ -177,24 +177,24 @@ class Statistics:
|
|
177
177
|
self.failed_frames = 0
|
178
178
|
|
179
179
|
|
180
|
-
@
|
181
|
-
@dataclass
|
180
|
+
@dataclass(slots=True, kw_only=True)
|
182
181
|
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
|
-
"""
|
182
|
+
"""Represents a device statistics."""
|
192
183
|
|
193
|
-
|
194
|
-
|
195
|
-
|
184
|
+
#: Device address
|
185
|
+
address: int
|
186
|
+
|
187
|
+
#: Datetime object representing connection time
|
188
|
+
connected_since: datetime = field(default_factory=datetime.now)
|
189
|
+
|
190
|
+
#: Datetime object representing time when device was last seen
|
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
|
196
196
|
|
197
|
-
async def update_last_seen(self,
|
197
|
+
async def update_last_seen(self, _: Any) -> None:
|
198
198
|
"""Update last seen property."""
|
199
199
|
self.last_seen = datetime.now()
|
200
200
|
|
@@ -249,10 +249,10 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
249
249
|
self.frame_producer(self._queues, reader=self.reader, writer=self.writer),
|
250
250
|
name="frame_producer_task",
|
251
251
|
)
|
252
|
-
for
|
252
|
+
for consumer_id in range(self.consumers_count):
|
253
253
|
self.create_task(
|
254
254
|
self.frame_consumer(self._queues.read),
|
255
|
-
name=f"frame_consumer_task ({
|
255
|
+
name=f"frame_consumer_task ({consumer_id})",
|
256
256
|
)
|
257
257
|
|
258
258
|
for device in self.data.values():
|
@@ -285,31 +285,39 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
285
285
|
await self._connection_close()
|
286
286
|
await asyncio.gather(*(device.shutdown() for device in self.data.values()))
|
287
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
|
+
|
288
305
|
async def frame_producer(
|
289
306
|
self, queues: Queues, reader: FrameReader, writer: FrameWriter
|
290
307
|
) -> None:
|
291
308
|
"""Handle frame reads and writes."""
|
292
|
-
statistics = self.statistics
|
293
309
|
await self.connected.wait()
|
294
310
|
while self.connected.is_set():
|
295
311
|
try:
|
296
|
-
request = None
|
297
312
|
if not queues.write.empty():
|
298
|
-
|
299
|
-
await writer.write(request)
|
300
|
-
queues.write.task_done()
|
301
|
-
|
302
|
-
if response := await reader.read():
|
303
|
-
queues.read.put_nowait(response)
|
304
|
-
|
305
|
-
statistics.update_transfer_statistics(request, response)
|
313
|
+
await self._write_from_queue(writer, queues.write)
|
306
314
|
|
315
|
+
await self._read_into_queue(reader, queues.read)
|
307
316
|
except ProtocolError as e:
|
308
|
-
statistics.failed_frames += 1
|
317
|
+
self.statistics.failed_frames += 1
|
309
318
|
_LOGGER.debug("Can't process received frame: %s", e)
|
310
319
|
except (OSError, asyncio.TimeoutError):
|
311
|
-
statistics.
|
312
|
-
statistics.connection_loss_at = datetime.now()
|
320
|
+
self.statistics.update_connection_lost()
|
313
321
|
self.create_task(self.connection_lost())
|
314
322
|
break
|
315
323
|
except Exception:
|
@@ -336,14 +344,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
336
344
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
337
345
|
device.dispatch_nowait(ATTR_SETUP, True)
|
338
346
|
await self.dispatch(name, device)
|
339
|
-
self.statistics.
|
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)
|
347
|
+
self.statistics.update_devices(device)
|
347
348
|
|
348
349
|
return self.data[name]
|
349
350
|
|
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.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
|
@@ -13,31 +13,28 @@ 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.7; extra == "test"
|
32
29
|
Requires-Dist: freezegun==1.5.5; extra == "test"
|
33
|
-
Requires-Dist: mypy==1.
|
30
|
+
Requires-Dist: mypy==1.18.2; 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.
|
37
|
-
Requires-Dist: pytest-asyncio==1.
|
38
|
-
Requires-Dist: ruff==0.
|
39
|
-
Requires-Dist: tox==4.
|
40
|
-
Requires-Dist: types-pyserial==3.5.0.
|
33
|
+
Requires-Dist: pytest==8.4.2; extra == "test"
|
34
|
+
Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
|
35
|
+
Requires-Dist: ruff==0.13.2; extra == "test"
|
36
|
+
Requires-Dist: tox==4.30.2; extra == "test"
|
37
|
+
Requires-Dist: types-pyserial==3.5.0.20250919; extra == "test"
|
41
38
|
Provides-Extra: docs
|
42
39
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
43
40
|
Requires-Dist: sphinx_rtd_theme==3.0.2; 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
|
|
@@ -87,6 +84,7 @@ through network by using RS-485 to Ethernet/WiFi converter.
|
|
87
84
|
- [Requests and Responses](https://pyplumio.denpa.pro/protocol.html#requests-and-responses)
|
88
85
|
- [Communication](https://pyplumio.denpa.pro/protocol.html#communication)
|
89
86
|
- [Versioning](https://pyplumio.denpa.pro/protocol.html#versioning)
|
87
|
+
- [Supported frames](https://pyplumio.denpa.pro/frames.html)
|
90
88
|
|
91
89
|
## Quickstart
|
92
90
|
|
@@ -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=7vNQiXfKffK0nbqts6Xy6-E1b1YOm4EGigvgaHr83o4,704
|
4
|
+
pyplumio/connection.py,sha256=4JoutupQSvAO8WXFFuwddpJJODzna5oq-cHJRI4kgZ8,6625
|
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=u0MqTFwbKv-kJeokXPgF2pNTVcFUExkH9MWIcY8ak7c,12083
|
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.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
|