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.
Files changed (48) hide show
  1. pyplumio/__init__.py +3 -1
  2. pyplumio/_version.py +2 -2
  3. pyplumio/const.py +0 -5
  4. pyplumio/data_types.py +2 -2
  5. pyplumio/devices/__init__.py +23 -5
  6. pyplumio/devices/ecomax.py +30 -53
  7. pyplumio/devices/ecoster.py +2 -3
  8. pyplumio/filters.py +199 -136
  9. pyplumio/frames/__init__.py +101 -15
  10. pyplumio/frames/messages.py +8 -65
  11. pyplumio/frames/requests.py +38 -38
  12. pyplumio/frames/responses.py +30 -86
  13. pyplumio/helpers/async_cache.py +13 -8
  14. pyplumio/helpers/event_manager.py +24 -18
  15. pyplumio/helpers/factory.py +0 -3
  16. pyplumio/parameters/__init__.py +38 -35
  17. pyplumio/protocol.py +14 -8
  18. pyplumio/structures/alerts.py +2 -2
  19. pyplumio/structures/ecomax_parameters.py +1 -1
  20. pyplumio/structures/frame_versions.py +3 -2
  21. pyplumio/structures/mixer_parameters.py +5 -3
  22. pyplumio/structures/network_info.py +1 -0
  23. pyplumio/structures/product_info.py +1 -1
  24. pyplumio/structures/program_version.py +2 -2
  25. pyplumio/structures/schedules.py +8 -40
  26. pyplumio/structures/sensor_data.py +498 -0
  27. pyplumio/structures/thermostat_parameters.py +7 -4
  28. pyplumio/utils.py +41 -4
  29. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/METADATA +4 -4
  30. pyplumio-0.6.2.dist-info/RECORD +50 -0
  31. pyplumio/structures/boiler_load.py +0 -32
  32. pyplumio/structures/boiler_power.py +0 -33
  33. pyplumio/structures/fan_power.py +0 -33
  34. pyplumio/structures/fuel_consumption.py +0 -36
  35. pyplumio/structures/fuel_level.py +0 -39
  36. pyplumio/structures/lambda_sensor.py +0 -57
  37. pyplumio/structures/mixer_sensors.py +0 -80
  38. pyplumio/structures/modules.py +0 -102
  39. pyplumio/structures/output_flags.py +0 -47
  40. pyplumio/structures/outputs.py +0 -88
  41. pyplumio/structures/pending_alerts.py +0 -28
  42. pyplumio/structures/statuses.py +0 -52
  43. pyplumio/structures/temperatures.py +0 -94
  44. pyplumio/structures/thermostat_sensors.py +0 -106
  45. pyplumio-0.6.1.dist-info/RECORD +0 -63
  46. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
  47. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
  48. {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
- Callback: TypeAlias = Callable[[], Awaitable[None]]
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[Callback]
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[Callback]:
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
- connected_since: datetime = field(default_factory=datetime.now)
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__ = ["Protocol", "DummyProtocol", "AsyncProtocol", "Statistics"]
357
+ __all__ = [
358
+ "Protocol",
359
+ "DummyProtocol",
360
+ "AsyncProtocol",
361
+ "Statistics",
362
+ "ConnectionLostCallback",
363
+ ]
@@ -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, None, None]:
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, (value + offset)
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], None, None]:
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
- version = UnsignedShort.from_bytes(message, self._offset + 1)
27
- self._offset += version.size + 1
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[tuple[int, ParameterValues], None, None]:
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[tuple[int, ParameterValues]]], None, None]:
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,
@@ -63,7 +63,7 @@ def format_model_name(model_name: str) -> str:
63
63
  return model_name
64
64
 
65
65
 
66
- @dataclass(frozen=True, slots=True)
66
+ @dataclass(frozen=True, slots=True, kw_only=True)
67
67
  class ProductInfo:
68
68
  """Represents a product info provided by an UID response."""
69
69
 
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  import struct
7
7
  from typing import Any, Final
8
8
 
9
- from pyplumio._version import __version_tuple__
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 __version_tuple__[0:3])
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
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Iterable, Iterator, MutableMapping, Sequence
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, reduce
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 Structure
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
- def _split_byte(byte: int) -> list[bool]:
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 _split_byte(message[i])
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.