PyPlumIO 0.5.42__py3-none-any.whl → 0.5.44__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 +3 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +14 -14
- pyplumio/const.py +8 -3
- pyplumio/{helpers/data_types.py → data_types.py} +23 -21
- pyplumio/devices/__init__.py +42 -41
- pyplumio/devices/ecomax.py +202 -174
- pyplumio/devices/ecoster.py +5 -0
- pyplumio/devices/mixer.py +24 -34
- pyplumio/devices/thermostat.py +24 -31
- pyplumio/filters.py +188 -147
- pyplumio/frames/__init__.py +20 -8
- pyplumio/frames/messages.py +3 -0
- pyplumio/frames/requests.py +21 -0
- pyplumio/frames/responses.py +18 -0
- pyplumio/helpers/async_cache.py +48 -0
- pyplumio/helpers/event_manager.py +58 -3
- pyplumio/helpers/factory.py +5 -2
- pyplumio/helpers/schedule.py +8 -5
- pyplumio/helpers/task_manager.py +3 -0
- pyplumio/helpers/timeout.py +7 -6
- pyplumio/helpers/uid.py +8 -5
- pyplumio/{helpers/parameter.py → parameters/__init__.py} +105 -5
- pyplumio/parameters/ecomax.py +868 -0
- pyplumio/parameters/mixer.py +245 -0
- pyplumio/parameters/thermostat.py +197 -0
- pyplumio/protocol.py +21 -10
- pyplumio/stream.py +3 -0
- pyplumio/structures/__init__.py +3 -0
- pyplumio/structures/alerts.py +9 -6
- pyplumio/structures/boiler_load.py +3 -0
- pyplumio/structures/boiler_power.py +4 -1
- pyplumio/structures/ecomax_parameters.py +6 -800
- pyplumio/structures/fan_power.py +4 -1
- pyplumio/structures/frame_versions.py +4 -1
- pyplumio/structures/fuel_consumption.py +4 -1
- pyplumio/structures/fuel_level.py +3 -0
- pyplumio/structures/lambda_sensor.py +9 -1
- pyplumio/structures/mixer_parameters.py +8 -230
- pyplumio/structures/mixer_sensors.py +10 -1
- pyplumio/structures/modules.py +14 -0
- pyplumio/structures/network_info.py +12 -1
- pyplumio/structures/output_flags.py +10 -1
- pyplumio/structures/outputs.py +22 -1
- pyplumio/structures/pending_alerts.py +3 -0
- pyplumio/structures/product_info.py +6 -3
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +5 -2
- pyplumio/structures/regulator_data_schema.py +4 -1
- pyplumio/structures/schedules.py +18 -1
- pyplumio/structures/statuses.py +9 -0
- pyplumio/structures/temperatures.py +23 -1
- pyplumio/structures/thermostat_parameters.py +18 -184
- pyplumio/structures/thermostat_sensors.py +10 -1
- pyplumio/utils.py +14 -12
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/METADATA +32 -17
- pyplumio-0.5.44.dist-info/RECORD +64 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
- pyplumio-0.5.42.dist-info/RECORD +0 -60
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/top_level.txt +0 -0
pyplumio/__init__.py
CHANGED
@@ -13,6 +13,7 @@ from pyplumio.exceptions import (
|
|
13
13
|
ProtocolError,
|
14
14
|
PyPlumIOError,
|
15
15
|
ReadError,
|
16
|
+
RequestError,
|
16
17
|
UnknownDeviceError,
|
17
18
|
UnknownFrameError,
|
18
19
|
)
|
@@ -90,6 +91,8 @@ def open_tcp_connection(
|
|
90
91
|
|
91
92
|
|
92
93
|
__all__ = [
|
94
|
+
"__version__",
|
95
|
+
"__version_tuple__",
|
93
96
|
"AsyncProtocol",
|
94
97
|
"ChecksumError",
|
95
98
|
"ConnectionFailedError",
|
@@ -107,8 +110,6 @@ __all__ = [
|
|
107
110
|
"UnknownDeviceError",
|
108
111
|
"UnknownFrameError",
|
109
112
|
"WirelessParameters",
|
110
|
-
"__version__",
|
111
|
-
"__version_tuple__",
|
112
113
|
"open_serial_connection",
|
113
114
|
"open_tcp_connection",
|
114
115
|
]
|
pyplumio/_version.py
CHANGED
pyplumio/connection.py
CHANGED
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
7
|
import logging
|
8
|
-
from typing import Any, Final
|
8
|
+
from typing import Any, Final
|
9
9
|
|
10
|
-
from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE
|
10
|
+
from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE
|
11
11
|
|
12
12
|
from pyplumio.exceptions import ConnectionFailedError
|
13
13
|
from pyplumio.helpers.task_manager import TaskManager
|
@@ -24,7 +24,7 @@ try:
|
|
24
24
|
|
25
25
|
_LOGGER.info("Using pyserial-asyncio-fast in place of pyserial-asyncio")
|
26
26
|
except ImportError:
|
27
|
-
import serial_asyncio as pyserial_asyncio
|
27
|
+
import serial_asyncio as pyserial_asyncio # type: ignore[no-redef]
|
28
28
|
|
29
29
|
|
30
30
|
class Connection(ABC, TaskManager):
|
@@ -73,7 +73,7 @@ class Connection(ABC, TaskManager):
|
|
73
73
|
try:
|
74
74
|
reader, writer = await self._open_connection()
|
75
75
|
self.protocol.connection_established(reader, writer)
|
76
|
-
except (OSError,
|
76
|
+
except (OSError, asyncio.TimeoutError) as err:
|
77
77
|
raise ConnectionFailedError from err
|
78
78
|
|
79
79
|
async def _reconnect(self) -> None:
|
@@ -184,14 +184,14 @@ class SerialConnection(Connection):
|
|
184
184
|
self,
|
185
185
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
186
186
|
"""Open the connection and return reader and writer objects."""
|
187
|
-
return
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
stopbits=STOPBITS_ONE,
|
195
|
-
**self._kwargs,
|
196
|
-
),
|
187
|
+
return await pyserial_asyncio.open_serial_connection(
|
188
|
+
url=self.device,
|
189
|
+
baudrate=self.baudrate,
|
190
|
+
bytesize=EIGHTBITS,
|
191
|
+
parity=PARITY_NONE,
|
192
|
+
stopbits=STOPBITS_ONE,
|
193
|
+
**self._kwargs,
|
197
194
|
)
|
195
|
+
|
196
|
+
|
197
|
+
__all__ = ["Connection", "TcpConnection", "SerialConnection"]
|
pyplumio/const.py
CHANGED
@@ -14,11 +14,11 @@ ATTR_CURRENT_TEMP: Final = "current_temp"
|
|
14
14
|
ATTR_DEVICE_INDEX: Final = "device_index"
|
15
15
|
ATTR_FRAME_ERRORS: Final = "frame_errors"
|
16
16
|
ATTR_INDEX: Final = "index"
|
17
|
-
ATTR_LOADED: Final = "loaded"
|
18
17
|
ATTR_OFFSET: Final = "offset"
|
19
18
|
ATTR_PARAMETER: Final = "parameter"
|
20
19
|
ATTR_PASSWORD: Final = "password"
|
21
20
|
ATTR_SCHEDULE: Final = "schedule"
|
21
|
+
ATTR_SETUP: Final = "setup"
|
22
22
|
ATTR_SENSORS: Final = "sensors"
|
23
23
|
ATTR_START: Final = "start"
|
24
24
|
ATTR_STATE: Final = "state"
|
@@ -91,6 +91,13 @@ class ProductType(IntEnum):
|
|
91
91
|
ECOMAX_I = 1
|
92
92
|
|
93
93
|
|
94
|
+
@unique
|
95
|
+
class ProductModel(Enum):
|
96
|
+
"""Contains device models."""
|
97
|
+
|
98
|
+
ECOMAX_860D3_HB = "ecoMAX 860D3-HB"
|
99
|
+
|
100
|
+
|
94
101
|
@unique
|
95
102
|
class AlertType(IntEnum):
|
96
103
|
"""Contains alert types."""
|
@@ -222,6 +229,4 @@ PERCENTAGE: Final = "%"
|
|
222
229
|
|
223
230
|
STATE_ON: Final = "on"
|
224
231
|
STATE_OFF: Final = "off"
|
225
|
-
|
226
|
-
|
227
232
|
State: TypeAlias = Literal["on", "off"]
|
@@ -54,15 +54,15 @@ class DataType(ABC, Generic[T]):
|
|
54
54
|
|
55
55
|
return NotImplemented
|
56
56
|
|
57
|
-
def
|
57
|
+
def _slice_to_size(self, buffer: bytes) -> bytes:
|
58
58
|
"""Slice the data to data type size."""
|
59
|
-
return
|
59
|
+
return buffer if self.size == 0 else buffer[: self.size]
|
60
60
|
|
61
61
|
@classmethod
|
62
|
-
def from_bytes(cls: type[DataTypeT],
|
62
|
+
def from_bytes(cls: type[DataTypeT], buffer: bytes, offset: int = 0) -> DataTypeT:
|
63
63
|
"""Initialize a new data type from bytes."""
|
64
64
|
data_type = cls()
|
65
|
-
data_type.unpack(
|
65
|
+
data_type.unpack(buffer[offset:])
|
66
66
|
return data_type
|
67
67
|
|
68
68
|
def to_bytes(self) -> bytes:
|
@@ -84,7 +84,7 @@ class DataType(ABC, Generic[T]):
|
|
84
84
|
"""Pack the data."""
|
85
85
|
|
86
86
|
@abstractmethod
|
87
|
-
def unpack(self,
|
87
|
+
def unpack(self, buffer: bytes) -> None:
|
88
88
|
"""Unpack the data."""
|
89
89
|
|
90
90
|
|
@@ -138,9 +138,9 @@ class BitArray(DataType[int]):
|
|
138
138
|
|
139
139
|
return b""
|
140
140
|
|
141
|
-
def unpack(self,
|
141
|
+
def unpack(self, buffer: bytes) -> None:
|
142
142
|
"""Unpack the data."""
|
143
|
-
self._value = UnsignedChar.from_bytes(
|
143
|
+
self._value = UnsignedChar.from_bytes(buffer[:1]).value
|
144
144
|
|
145
145
|
@property
|
146
146
|
def value(self) -> bool:
|
@@ -170,9 +170,9 @@ class IPv4(DataType[str]):
|
|
170
170
|
"""Pack the data."""
|
171
171
|
return socket.inet_aton(self.value)
|
172
172
|
|
173
|
-
def unpack(self,
|
173
|
+
def unpack(self, buffer: bytes) -> None:
|
174
174
|
"""Unpack the data."""
|
175
|
-
self._value = socket.inet_ntoa(self.
|
175
|
+
self._value = socket.inet_ntoa(self._slice_to_size(buffer))
|
176
176
|
|
177
177
|
|
178
178
|
class IPv6(DataType[str]):
|
@@ -189,9 +189,9 @@ class IPv6(DataType[str]):
|
|
189
189
|
"""Pack the data."""
|
190
190
|
return socket.inet_pton(socket.AF_INET6, self.value)
|
191
191
|
|
192
|
-
def unpack(self,
|
192
|
+
def unpack(self, buffer: bytes) -> None:
|
193
193
|
"""Unpack the data."""
|
194
|
-
self._value = socket.inet_ntop(socket.AF_INET6, self.
|
194
|
+
self._value = socket.inet_ntop(socket.AF_INET6, self._slice_to_size(buffer))
|
195
195
|
|
196
196
|
|
197
197
|
class String(DataType[str]):
|
@@ -208,9 +208,9 @@ class String(DataType[str]):
|
|
208
208
|
"""Pack the data."""
|
209
209
|
return self.value.encode() + b"\0"
|
210
210
|
|
211
|
-
def unpack(self,
|
211
|
+
def unpack(self, buffer: bytes) -> None:
|
212
212
|
"""Unpack the data."""
|
213
|
-
self._value =
|
213
|
+
self._value = buffer.split(b"\0", 1)[0].decode("utf-8", "replace")
|
214
214
|
self._size = len(self.value) + 1
|
215
215
|
|
216
216
|
|
@@ -228,10 +228,10 @@ class VarBytes(DataType[bytes]):
|
|
228
228
|
"""Pack the data."""
|
229
229
|
return UnsignedChar(self.size - 1).to_bytes() + self.value
|
230
230
|
|
231
|
-
def unpack(self,
|
231
|
+
def unpack(self, buffer: bytes) -> None:
|
232
232
|
"""Unpack the data."""
|
233
|
-
self._size =
|
234
|
-
self._value =
|
233
|
+
self._size = buffer[0] + 1
|
234
|
+
self._value = buffer[1 : self.size]
|
235
235
|
|
236
236
|
|
237
237
|
class VarString(DataType[str]):
|
@@ -248,10 +248,10 @@ class VarString(DataType[str]):
|
|
248
248
|
"""Pack the data."""
|
249
249
|
return UnsignedChar(self.size - 1).to_bytes() + self.value.encode()
|
250
250
|
|
251
|
-
def unpack(self,
|
251
|
+
def unpack(self, buffer: bytes) -> None:
|
252
252
|
"""Unpack the data."""
|
253
|
-
self._size =
|
254
|
-
self._value =
|
253
|
+
self._size = buffer[0] + 1
|
254
|
+
self._value = buffer[1 : self.size].decode("utf-8", "replace")
|
255
255
|
|
256
256
|
|
257
257
|
class BuiltInDataType(DataType[T], ABC):
|
@@ -265,9 +265,9 @@ class BuiltInDataType(DataType[T], ABC):
|
|
265
265
|
"""Pack the data."""
|
266
266
|
return self._struct.pack(self.value)
|
267
267
|
|
268
|
-
def unpack(self,
|
268
|
+
def unpack(self, buffer: bytes) -> None:
|
269
269
|
"""Unpack the data."""
|
270
|
-
self._value = self._struct.unpack_from(
|
270
|
+
self._value = self._struct.unpack_from(buffer)[0]
|
271
271
|
|
272
272
|
@property
|
273
273
|
def size(self) -> int:
|
@@ -379,3 +379,5 @@ DATA_TYPES: tuple[type[DataType], ...] = (
|
|
379
379
|
IPv4,
|
380
380
|
IPv6,
|
381
381
|
)
|
382
|
+
|
383
|
+
__all__ = ["DataType"] + list({dt.__name__ for dt in DATA_TYPES})
|
pyplumio/devices/__init__.py
CHANGED
@@ -8,13 +8,13 @@ from functools import cache
|
|
8
8
|
import logging
|
9
9
|
from typing import Any, ClassVar
|
10
10
|
|
11
|
-
from pyplumio.const import ATTR_FRAME_ERRORS,
|
11
|
+
from pyplumio.const import ATTR_FRAME_ERRORS, DeviceType, FrameType, State
|
12
12
|
from pyplumio.exceptions import RequestError, UnknownDeviceError
|
13
|
-
from pyplumio.
|
14
|
-
from pyplumio.
|
13
|
+
from pyplumio.filters import on_change
|
14
|
+
from pyplumio.frames import Frame, Request, is_known_frame_type
|
15
|
+
from pyplumio.helpers.event_manager import EventManager, event_listener
|
15
16
|
from pyplumio.helpers.factory import create_instance
|
16
|
-
from pyplumio.
|
17
|
-
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
17
|
+
from pyplumio.parameters import NumericType, Parameter
|
18
18
|
from pyplumio.structures.network_info import NetworkInfo
|
19
19
|
from pyplumio.utils import to_camelcase
|
20
20
|
|
@@ -47,6 +47,8 @@ def get_device_handler(device_type: int) -> str:
|
|
47
47
|
class Device(ABC, EventManager):
|
48
48
|
"""Represents a device."""
|
49
49
|
|
50
|
+
__slots__ = ("queue",)
|
51
|
+
|
50
52
|
queue: asyncio.Queue[Frame]
|
51
53
|
|
52
54
|
def __init__(self, queue: asyncio.Queue[Frame]) -> None:
|
@@ -123,9 +125,11 @@ class PhysicalDevice(Device, ABC):
|
|
123
125
|
virtual devices associated with them via parent property.
|
124
126
|
"""
|
125
127
|
|
128
|
+
__slots__ = ("address", "_network", "_frame_versions")
|
129
|
+
|
126
130
|
address: ClassVar[int]
|
131
|
+
|
127
132
|
_network: NetworkInfo
|
128
|
-
_setup_frames: tuple[DataFrameDescription, ...]
|
129
133
|
_frame_versions: dict[int, int]
|
130
134
|
|
131
135
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
@@ -134,22 +138,26 @@ class PhysicalDevice(Device, ABC):
|
|
134
138
|
self._network = network
|
135
139
|
self._frame_versions = {}
|
136
140
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
141
|
+
@event_listener(filter=on_change)
|
142
|
+
async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
|
143
|
+
"""Check frame versions and update outdated frames."""
|
144
|
+
_LOGGER.info("Received version table")
|
145
|
+
for frame_type, version in versions.items():
|
146
|
+
if (
|
147
|
+
is_known_frame_type(frame_type)
|
148
|
+
and self.supports_frame_type(frame_type)
|
149
|
+
and not self.has_frame_version(frame_type, version)
|
150
|
+
):
|
151
|
+
await self._request_frame_version(frame_type, version)
|
152
|
+
self._frame_versions[frame_type] = version
|
153
|
+
|
154
|
+
async def _request_frame_version(
|
155
|
+
self, frame_type: FrameType | int, version: int
|
156
|
+
) -> None:
|
157
|
+
"""Request frame version from the device."""
|
158
|
+
_LOGGER.info("Updating frame %s to version %i", repr(frame_type), version)
|
159
|
+
request = await Request.create(frame_type, recipient=self.address)
|
160
|
+
self.queue.put_nowait(request)
|
153
161
|
|
154
162
|
def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
|
155
163
|
"""Return True if frame data is up to date, False otherwise."""
|
@@ -169,25 +177,6 @@ class PhysicalDevice(Device, ABC):
|
|
169
177
|
for name, value in frame.data.items():
|
170
178
|
self.dispatch_nowait(name, value)
|
171
179
|
|
172
|
-
async def async_setup(self) -> bool:
|
173
|
-
"""Set up addressable device."""
|
174
|
-
results = await asyncio.gather(
|
175
|
-
*(
|
176
|
-
self.request(description.provides, description.frame_type)
|
177
|
-
for description in self._setup_frames
|
178
|
-
),
|
179
|
-
return_exceptions=True,
|
180
|
-
)
|
181
|
-
|
182
|
-
errors = [
|
183
|
-
result.frame_type for result in results if isinstance(result, RequestError)
|
184
|
-
]
|
185
|
-
|
186
|
-
await asyncio.gather(
|
187
|
-
self.dispatch(ATTR_FRAME_ERRORS, errors), self.dispatch(ATTR_LOADED, True)
|
188
|
-
)
|
189
|
-
return True
|
190
|
-
|
191
180
|
async def request(
|
192
181
|
self, name: str, frame_type: FrameType, retries: int = 3, timeout: float = 3.0
|
193
182
|
) -> Any:
|
@@ -195,6 +184,7 @@ class PhysicalDevice(Device, ABC):
|
|
195
184
|
|
196
185
|
If value is not available before timeout, retry request.
|
197
186
|
"""
|
187
|
+
_LOGGER.info("Requesting '%s' with %s", name, repr(frame_type))
|
198
188
|
request = await Request.create(frame_type, recipient=self.address)
|
199
189
|
while retries > 0:
|
200
190
|
try:
|
@@ -218,6 +208,8 @@ class PhysicalDevice(Device, ABC):
|
|
218
208
|
class VirtualDevice(Device, ABC):
|
219
209
|
"""Represents a virtual device associated with physical device."""
|
220
210
|
|
211
|
+
__slots__ = ("parent", "index")
|
212
|
+
|
221
213
|
parent: PhysicalDevice
|
222
214
|
index: int
|
223
215
|
|
@@ -228,3 +220,12 @@ class VirtualDevice(Device, ABC):
|
|
228
220
|
super().__init__(queue)
|
229
221
|
self.parent = parent
|
230
222
|
self.index = index
|
223
|
+
|
224
|
+
|
225
|
+
__all__ = [
|
226
|
+
"Device",
|
227
|
+
"PhysicalDevice",
|
228
|
+
"VirtualDevice",
|
229
|
+
"is_known_device_type",
|
230
|
+
"get_device_handler",
|
231
|
+
]
|