PyPlumIO 0.6.1__py3-none-any.whl → 0.6.2__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 -1
- pyplumio/_version.py +2 -2
- pyplumio/const.py +0 -5
- pyplumio/data_types.py +2 -2
- pyplumio/devices/__init__.py +23 -5
- pyplumio/devices/ecomax.py +30 -53
- pyplumio/devices/ecoster.py +2 -3
- pyplumio/filters.py +199 -136
- pyplumio/frames/__init__.py +101 -15
- pyplumio/frames/messages.py +8 -65
- pyplumio/frames/requests.py +38 -38
- pyplumio/frames/responses.py +30 -86
- pyplumio/helpers/async_cache.py +13 -8
- pyplumio/helpers/event_manager.py +24 -18
- pyplumio/helpers/factory.py +0 -3
- pyplumio/parameters/__init__.py +38 -35
- pyplumio/protocol.py +14 -8
- pyplumio/structures/alerts.py +2 -2
- pyplumio/structures/ecomax_parameters.py +1 -1
- pyplumio/structures/frame_versions.py +3 -2
- pyplumio/structures/mixer_parameters.py +5 -3
- pyplumio/structures/network_info.py +1 -0
- pyplumio/structures/product_info.py +1 -1
- pyplumio/structures/program_version.py +2 -2
- pyplumio/structures/schedules.py +8 -40
- pyplumio/structures/sensor_data.py +498 -0
- pyplumio/structures/thermostat_parameters.py +7 -4
- pyplumio/utils.py +41 -4
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/METADATA +4 -4
- pyplumio-0.6.2.dist-info/RECORD +50 -0
- pyplumio/structures/boiler_load.py +0 -32
- pyplumio/structures/boiler_power.py +0 -33
- pyplumio/structures/fan_power.py +0 -33
- pyplumio/structures/fuel_consumption.py +0 -36
- pyplumio/structures/fuel_level.py +0 -39
- pyplumio/structures/lambda_sensor.py +0 -57
- pyplumio/structures/mixer_sensors.py +0 -80
- pyplumio/structures/modules.py +0 -102
- pyplumio/structures/output_flags.py +0 -47
- pyplumio/structures/outputs.py +0 -88
- pyplumio/structures/pending_alerts.py +0 -28
- pyplumio/structures/statuses.py +0 -52
- pyplumio/structures/temperatures.py +0 -94
- pyplumio/structures/thermostat_sensors.py +0 -106
- pyplumio-0.6.1.dist-info/RECORD +0 -63
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/top_level.txt +0 -0
pyplumio/protocol.py
CHANGED
@@ -27,7 +27,7 @@ from pyplumio.structures.regulator_data import ATTR_REGDATA
|
|
27
27
|
|
28
28
|
_LOGGER = logging.getLogger(__name__)
|
29
29
|
|
30
|
-
|
30
|
+
ConnectionLostCallback: TypeAlias = Callable[[], Awaitable[None]]
|
31
31
|
|
32
32
|
|
33
33
|
class Protocol(ABC):
|
@@ -36,7 +36,7 @@ class Protocol(ABC):
|
|
36
36
|
connected: asyncio.Event
|
37
37
|
reader: FrameReader | None
|
38
38
|
writer: FrameWriter | None
|
39
|
-
_on_connection_lost: set[
|
39
|
+
_on_connection_lost: set[ConnectionLostCallback]
|
40
40
|
|
41
41
|
def __init__(self) -> None:
|
42
42
|
"""Initialize a new protocol."""
|
@@ -53,7 +53,7 @@ class Protocol(ABC):
|
|
53
53
|
self.writer = None
|
54
54
|
|
55
55
|
@property
|
56
|
-
def on_connection_lost(self) -> set[
|
56
|
+
def on_connection_lost(self) -> set[ConnectionLostCallback]:
|
57
57
|
"""Return the callbacks that'll be called on connection lost."""
|
58
58
|
return self._on_connection_lost
|
59
59
|
|
@@ -105,8 +105,8 @@ class DummyProtocol(Protocol):
|
|
105
105
|
class Queues:
|
106
106
|
"""Represents asyncio queues."""
|
107
107
|
|
108
|
-
read: asyncio.Queue[Frame]
|
109
|
-
write: asyncio.Queue[Frame]
|
108
|
+
read: asyncio.Queue[Frame] = field(default_factory=asyncio.Queue)
|
109
|
+
write: asyncio.Queue[Frame] = field(default_factory=asyncio.Queue)
|
110
110
|
|
111
111
|
async def join(self) -> None:
|
112
112
|
"""Wait for queues to finish."""
|
@@ -185,7 +185,7 @@ class DeviceStatistics:
|
|
185
185
|
address: int
|
186
186
|
|
187
187
|
#: Datetime object representing connection time
|
188
|
-
|
188
|
+
first_seen: datetime = field(default_factory=datetime.now)
|
189
189
|
|
190
190
|
#: Datetime object representing time when device was last seen
|
191
191
|
last_seen: datetime = field(default_factory=datetime.now)
|
@@ -234,9 +234,9 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
234
234
|
eth=ethernet_parameters or EthernetParameters(status=False),
|
235
235
|
wlan=wireless_parameters or WirelessParameters(status=False),
|
236
236
|
)
|
237
|
-
self._queues = Queues(read=asyncio.Queue(), write=asyncio.Queue())
|
238
237
|
self._entry_lock = asyncio.Lock()
|
239
238
|
self._statistics = Statistics()
|
239
|
+
self._queues = Queues()
|
240
240
|
|
241
241
|
def connection_established(
|
242
242
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
@@ -354,4 +354,10 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
354
354
|
return self._statistics
|
355
355
|
|
356
356
|
|
357
|
-
__all__ = [
|
357
|
+
__all__ = [
|
358
|
+
"Protocol",
|
359
|
+
"DummyProtocol",
|
360
|
+
"AsyncProtocol",
|
361
|
+
"Statistics",
|
362
|
+
"ConnectionLostCallback",
|
363
|
+
]
|
pyplumio/structures/alerts.py
CHANGED
@@ -46,12 +46,12 @@ def seconds_to_datetime(timestamp: int) -> datetime:
|
|
46
46
|
in seconds counted from Jan 1st, 2000.
|
47
47
|
"""
|
48
48
|
|
49
|
-
def datetime_kwargs(timestamp: int) -> Generator[Any,
|
49
|
+
def datetime_kwargs(timestamp: int) -> Generator[tuple[Any, int]]:
|
50
50
|
"""Yield a tuple, that represents a single datetime kwarg."""
|
51
51
|
for name, seconds, offset in DATETIME_INTERVALS:
|
52
52
|
value = timestamp // seconds
|
53
53
|
timestamp -= value * seconds
|
54
|
-
yield name,
|
54
|
+
yield name, value + offset
|
55
55
|
|
56
56
|
return datetime(**dict(datetime_kwargs(timestamp)))
|
57
57
|
|
@@ -24,7 +24,7 @@ class EcomaxParametersStructure(StructureDecoder):
|
|
24
24
|
|
25
25
|
def _ecomax_parameter(
|
26
26
|
self, message: bytearray, start: int, end: int
|
27
|
-
) -> Generator[tuple[int, ParameterValues]
|
27
|
+
) -> Generator[tuple[int, ParameterValues]]:
|
28
28
|
"""Unpack an ecoMAX parameter."""
|
29
29
|
for index in range(start, start + end):
|
30
30
|
if parameter := unpack_parameter(message, self._offset):
|
@@ -23,8 +23,9 @@ class FrameVersionsStructure(StructureDecoder):
|
|
23
23
|
def _unpack_frame_versions(self, message: bytearray) -> tuple[FrameType | int, int]:
|
24
24
|
"""Unpack frame versions."""
|
25
25
|
frame_type = message[self._offset]
|
26
|
-
|
27
|
-
|
26
|
+
self._offset += 1
|
27
|
+
version = UnsignedShort.from_bytes(message, self._offset)
|
28
|
+
self._offset += version.size
|
28
29
|
with suppress(ValueError):
|
29
30
|
frame_type = FrameType(frame_type)
|
30
31
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from collections.abc import Generator
|
6
|
-
from typing import Any, Final
|
6
|
+
from typing import Any, Final, TypeAlias
|
7
7
|
|
8
8
|
from pyplumio.parameters import ParameterValues, unpack_parameter
|
9
9
|
from pyplumio.structures import StructureDecoder
|
@@ -13,6 +13,8 @@ ATTR_MIXER_PARAMETERS: Final = "mixer_parameters"
|
|
13
13
|
|
14
14
|
MIXER_PARAMETER_SIZE: Final = 3
|
15
15
|
|
16
|
+
_ParameterValues: TypeAlias = tuple[int, ParameterValues]
|
17
|
+
|
16
18
|
|
17
19
|
class MixerParametersStructure(StructureDecoder):
|
18
20
|
"""Represents a mixer parameters data structure."""
|
@@ -23,7 +25,7 @@ class MixerParametersStructure(StructureDecoder):
|
|
23
25
|
|
24
26
|
def _mixer_parameter(
|
25
27
|
self, message: bytearray, start: int, end: int
|
26
|
-
) -> Generator[
|
28
|
+
) -> Generator[_ParameterValues]:
|
27
29
|
"""Get a single mixer parameter."""
|
28
30
|
for index in range(start, start + end):
|
29
31
|
if parameter := unpack_parameter(message, self._offset):
|
@@ -33,7 +35,7 @@ class MixerParametersStructure(StructureDecoder):
|
|
33
35
|
|
34
36
|
def _mixer_parameters(
|
35
37
|
self, message: bytearray, mixers: int, start: int, end: int
|
36
|
-
) -> Generator[tuple[int, list[
|
38
|
+
) -> Generator[tuple[int, list[_ParameterValues]]]:
|
37
39
|
"""Get parameters for a mixer."""
|
38
40
|
for index in range(mixers):
|
39
41
|
if parameters := list(self._mixer_parameter(message, start, end)):
|
@@ -88,6 +88,7 @@ class NetworkInfoStructure(Structure):
|
|
88
88
|
self, message: bytearray, offset: int = 0, data: dict[str, Any] | None = None
|
89
89
|
) -> tuple[dict[str, Any], int]:
|
90
90
|
"""Decode bytes and return message data and offset."""
|
91
|
+
offset += 1
|
91
92
|
return (
|
92
93
|
ensure_dict(
|
93
94
|
data,
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|
6
6
|
import struct
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
|
-
from pyplumio
|
9
|
+
from pyplumio import version_tuple
|
10
10
|
from pyplumio.structures import Structure
|
11
11
|
from pyplumio.utils import ensure_dict
|
12
12
|
|
@@ -14,7 +14,7 @@ ATTR_VERSION: Final = "version"
|
|
14
14
|
|
15
15
|
VERSION_INFO_SIZE: Final = 15
|
16
16
|
|
17
|
-
SOFTWARE_VERSION: Final = ".".join(str(x) for x in
|
17
|
+
SOFTWARE_VERSION: Final = ".".join(str(x) for x in version_tuple[0:3])
|
18
18
|
|
19
19
|
struct_program_version = struct.Struct("<2sB2s3s3HB")
|
20
20
|
|
pyplumio/structures/schedules.py
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from collections.abc import Iterable, Iterator, MutableMapping
|
5
|
+
from collections.abc import Iterable, Iterator, MutableMapping
|
6
6
|
from dataclasses import dataclass
|
7
7
|
import datetime as dt
|
8
|
-
from functools import lru_cache
|
9
|
-
from typing import Annotated, Any, Final, get_args
|
8
|
+
from functools import lru_cache
|
9
|
+
from typing import Annotated, Any, Final, TypeAlias, get_args
|
10
10
|
|
11
11
|
from pyplumio.const import (
|
12
12
|
ATTR_PARAMETER,
|
@@ -19,7 +19,6 @@ from pyplumio.const import (
|
|
19
19
|
State,
|
20
20
|
)
|
21
21
|
from pyplumio.devices import Device, PhysicalDevice
|
22
|
-
from pyplumio.exceptions import FrameDataError
|
23
22
|
from pyplumio.frames import Request
|
24
23
|
from pyplumio.parameters import (
|
25
24
|
Number,
|
@@ -31,8 +30,8 @@ from pyplumio.parameters import (
|
|
31
30
|
SwitchDescription,
|
32
31
|
unpack_parameter,
|
33
32
|
)
|
34
|
-
from pyplumio.structures import
|
35
|
-
from pyplumio.utils import ensure_dict
|
33
|
+
from pyplumio.structures import StructureDecoder
|
34
|
+
from pyplumio.utils import ensure_dict, split_byte
|
36
35
|
|
37
36
|
ATTR_SCHEDULES: Final = "schedules"
|
38
37
|
ATTR_SCHEDULE_PARAMETERS: Final = "schedule_parameters"
|
@@ -156,7 +155,7 @@ def collect_schedule_data(name: str, device: Device) -> dict[str, Any]:
|
|
156
155
|
|
157
156
|
TIME_FORMAT: Final = "%H:%M"
|
158
157
|
|
159
|
-
Time = Annotated[str, "Time string in %H:%M format"]
|
158
|
+
Time: TypeAlias = Annotated[str, "Time string in %H:%M format"]
|
160
159
|
|
161
160
|
MIDNIGHT: Final = Time("00:00")
|
162
161
|
MIDNIGHT_DT = dt.datetime.strptime(MIDNIGHT, TIME_FORMAT)
|
@@ -305,51 +304,20 @@ class Schedule(Iterable):
|
|
305
304
|
)
|
306
305
|
|
307
306
|
|
308
|
-
|
309
|
-
"""Split single byte into an eight bits."""
|
310
|
-
return [bool(byte & (1 << bit)) for bit in reversed(range(8))]
|
311
|
-
|
312
|
-
|
313
|
-
def _join_bits(bits: Sequence[int | bool]) -> int:
|
314
|
-
"""Join eight bits into a single byte."""
|
315
|
-
return reduce(lambda bit, byte: (bit << 1) | byte, bits)
|
316
|
-
|
317
|
-
|
318
|
-
class SchedulesStructure(Structure):
|
307
|
+
class SchedulesStructure(StructureDecoder):
|
319
308
|
"""Represents a schedule data structure."""
|
320
309
|
|
321
310
|
__slots__ = ("_offset",)
|
322
311
|
|
323
312
|
_offset: int
|
324
313
|
|
325
|
-
def encode(self, data: dict[str, Any]) -> bytearray:
|
326
|
-
"""Encode data to the bytearray message."""
|
327
|
-
try:
|
328
|
-
header = bytearray(
|
329
|
-
b"\1"
|
330
|
-
+ SCHEDULES.index(data[ATTR_TYPE]).to_bytes(
|
331
|
-
length=1, byteorder="little"
|
332
|
-
)
|
333
|
-
+ int(data[ATTR_SWITCH]).to_bytes(length=1, byteorder="little")
|
334
|
-
+ int(data[ATTR_PARAMETER]).to_bytes(length=1, byteorder="little")
|
335
|
-
)
|
336
|
-
schedule = data[ATTR_SCHEDULE]
|
337
|
-
except (KeyError, ValueError) as e:
|
338
|
-
raise FrameDataError from e
|
339
|
-
|
340
|
-
return header + bytearray(
|
341
|
-
_join_bits(day[i : i + 8])
|
342
|
-
for day in schedule
|
343
|
-
for i in range(0, len(day), 8)
|
344
|
-
)
|
345
|
-
|
346
314
|
def _unpack_schedule(self, message: bytearray) -> list[list[bool]]:
|
347
315
|
"""Unpack a schedule."""
|
348
316
|
offset = self._offset
|
349
317
|
schedule = [
|
350
318
|
bit
|
351
319
|
for i in range(offset, offset + SCHEDULE_SIZE)
|
352
|
-
for bit in
|
320
|
+
for bit in split_byte(message[i])
|
353
321
|
]
|
354
322
|
self._offset = offset + SCHEDULE_SIZE
|
355
323
|
# Split the schedule. Each day consists of 48 half-hour intervals.
|