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.
Files changed (61) hide show
  1. pyplumio/__init__.py +3 -2
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +14 -14
  4. pyplumio/const.py +8 -3
  5. pyplumio/{helpers/data_types.py → data_types.py} +23 -21
  6. pyplumio/devices/__init__.py +42 -41
  7. pyplumio/devices/ecomax.py +202 -174
  8. pyplumio/devices/ecoster.py +5 -0
  9. pyplumio/devices/mixer.py +24 -34
  10. pyplumio/devices/thermostat.py +24 -31
  11. pyplumio/filters.py +188 -147
  12. pyplumio/frames/__init__.py +20 -8
  13. pyplumio/frames/messages.py +3 -0
  14. pyplumio/frames/requests.py +21 -0
  15. pyplumio/frames/responses.py +18 -0
  16. pyplumio/helpers/async_cache.py +48 -0
  17. pyplumio/helpers/event_manager.py +58 -3
  18. pyplumio/helpers/factory.py +5 -2
  19. pyplumio/helpers/schedule.py +8 -5
  20. pyplumio/helpers/task_manager.py +3 -0
  21. pyplumio/helpers/timeout.py +7 -6
  22. pyplumio/helpers/uid.py +8 -5
  23. pyplumio/{helpers/parameter.py → parameters/__init__.py} +105 -5
  24. pyplumio/parameters/ecomax.py +868 -0
  25. pyplumio/parameters/mixer.py +245 -0
  26. pyplumio/parameters/thermostat.py +197 -0
  27. pyplumio/protocol.py +21 -10
  28. pyplumio/stream.py +3 -0
  29. pyplumio/structures/__init__.py +3 -0
  30. pyplumio/structures/alerts.py +9 -6
  31. pyplumio/structures/boiler_load.py +3 -0
  32. pyplumio/structures/boiler_power.py +4 -1
  33. pyplumio/structures/ecomax_parameters.py +6 -800
  34. pyplumio/structures/fan_power.py +4 -1
  35. pyplumio/structures/frame_versions.py +4 -1
  36. pyplumio/structures/fuel_consumption.py +4 -1
  37. pyplumio/structures/fuel_level.py +3 -0
  38. pyplumio/structures/lambda_sensor.py +9 -1
  39. pyplumio/structures/mixer_parameters.py +8 -230
  40. pyplumio/structures/mixer_sensors.py +10 -1
  41. pyplumio/structures/modules.py +14 -0
  42. pyplumio/structures/network_info.py +12 -1
  43. pyplumio/structures/output_flags.py +10 -1
  44. pyplumio/structures/outputs.py +22 -1
  45. pyplumio/structures/pending_alerts.py +3 -0
  46. pyplumio/structures/product_info.py +6 -3
  47. pyplumio/structures/program_version.py +3 -0
  48. pyplumio/structures/regulator_data.py +5 -2
  49. pyplumio/structures/regulator_data_schema.py +4 -1
  50. pyplumio/structures/schedules.py +18 -1
  51. pyplumio/structures/statuses.py +9 -0
  52. pyplumio/structures/temperatures.py +23 -1
  53. pyplumio/structures/thermostat_parameters.py +18 -184
  54. pyplumio/structures/thermostat_sensors.py +10 -1
  55. pyplumio/utils.py +14 -12
  56. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/METADATA +32 -17
  57. pyplumio-0.5.44.dist-info/RECORD +64 -0
  58. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
  59. pyplumio-0.5.42.dist-info/RECORD +0 -60
  60. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
  61. {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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.42'
21
- __version_tuple__ = version_tuple = (0, 5, 42)
20
+ __version__ = version = '0.5.44'
21
+ __version_tuple__ = version_tuple = (0, 5, 44)
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, cast
8
+ from typing import Any, Final
9
9
 
10
- from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE, SerialException
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, SerialException, asyncio.TimeoutError) as err:
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 cast(
188
- tuple[asyncio.StreamReader, asyncio.StreamWriter],
189
- await pyserial_asyncio.open_serial_connection(
190
- url=self.device,
191
- baudrate=self.baudrate,
192
- bytesize=EIGHTBITS,
193
- parity=PARITY_NONE,
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 _slice_data(self, data: bytes) -> bytes:
57
+ def _slice_to_size(self, buffer: bytes) -> bytes:
58
58
  """Slice the data to data type size."""
59
- return data if self.size == 0 else data[: self.size]
59
+ return buffer if self.size == 0 else buffer[: self.size]
60
60
 
61
61
  @classmethod
62
- def from_bytes(cls: type[DataTypeT], data: bytes, offset: int = 0) -> 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(data[offset:])
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, data: bytes) -> None:
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, data: bytes) -> None:
141
+ def unpack(self, buffer: bytes) -> None:
142
142
  """Unpack the data."""
143
- self._value = UnsignedChar.from_bytes(data[:1]).value
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, data: bytes) -> None:
173
+ def unpack(self, buffer: bytes) -> None:
174
174
  """Unpack the data."""
175
- self._value = socket.inet_ntoa(self._slice_data(data))
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, data: bytes) -> None:
192
+ def unpack(self, buffer: bytes) -> None:
193
193
  """Unpack the data."""
194
- self._value = socket.inet_ntop(socket.AF_INET6, self._slice_data(data))
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, data: bytes) -> None:
211
+ def unpack(self, buffer: bytes) -> None:
212
212
  """Unpack the data."""
213
- self._value = data.split(b"\0", 1)[0].decode("utf-8", "replace")
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, data: bytes) -> None:
231
+ def unpack(self, buffer: bytes) -> None:
232
232
  """Unpack the data."""
233
- self._size = data[0] + 1
234
- self._value = data[1 : self.size]
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, data: bytes) -> None:
251
+ def unpack(self, buffer: bytes) -> None:
252
252
  """Unpack the data."""
253
- self._size = data[0] + 1
254
- self._value = data[1 : self.size].decode("utf-8", "replace")
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, data: bytes) -> None:
268
+ def unpack(self, buffer: bytes) -> None:
269
269
  """Unpack the data."""
270
- self._value = self._struct.unpack_from(data)[0]
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})
@@ -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, ATTR_LOADED, DeviceType, FrameType
11
+ from pyplumio.const import ATTR_FRAME_ERRORS, DeviceType, FrameType, State
12
12
  from pyplumio.exceptions import RequestError, UnknownDeviceError
13
- from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
14
- from pyplumio.helpers.event_manager import EventManager
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.helpers.parameter import NumericType, Parameter, State
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
- async def update_frame_versions(versions: dict[int, int]) -> None:
138
- """Check frame versions and update outdated frames."""
139
- for frame_type, version in versions.items():
140
- if (
141
- is_known_frame_type(frame_type)
142
- and self.supports_frame_type(frame_type)
143
- and not self.has_frame_version(frame_type, version)
144
- ):
145
- _LOGGER.debug(
146
- "Updating frame %s to version %i", frame_type, version
147
- )
148
- request = await Request.create(frame_type, recipient=self.address)
149
- self.queue.put_nowait(request)
150
- self._frame_versions[frame_type] = version
151
-
152
- self.subscribe(ATTR_FRAME_VERSIONS, update_frame_versions)
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
+ ]