PyPlumIO 0.5.41__py3-none-any.whl → 0.5.43__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 (60) hide show
  1. pyplumio/__init__.py +3 -2
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +14 -14
  4. pyplumio/const.py +7 -0
  5. pyplumio/devices/__init__.py +32 -19
  6. pyplumio/devices/ecomax.py +112 -128
  7. pyplumio/devices/ecoster.py +5 -0
  8. pyplumio/devices/mixer.py +21 -31
  9. pyplumio/devices/thermostat.py +19 -29
  10. pyplumio/filters.py +166 -147
  11. pyplumio/frames/__init__.py +20 -8
  12. pyplumio/frames/messages.py +3 -0
  13. pyplumio/frames/requests.py +21 -0
  14. pyplumio/frames/responses.py +18 -0
  15. pyplumio/helpers/data_types.py +23 -21
  16. pyplumio/helpers/event_manager.py +40 -3
  17. pyplumio/helpers/factory.py +5 -2
  18. pyplumio/helpers/schedule.py +8 -5
  19. pyplumio/helpers/task_manager.py +3 -0
  20. pyplumio/helpers/timeout.py +8 -8
  21. pyplumio/helpers/uid.py +8 -5
  22. pyplumio/{helpers/parameter.py → parameters/__init__.py} +111 -8
  23. pyplumio/parameters/ecomax.py +868 -0
  24. pyplumio/parameters/mixer.py +245 -0
  25. pyplumio/parameters/thermostat.py +197 -0
  26. pyplumio/protocol.py +6 -3
  27. pyplumio/stream.py +3 -0
  28. pyplumio/structures/__init__.py +3 -0
  29. pyplumio/structures/alerts.py +8 -5
  30. pyplumio/structures/boiler_load.py +3 -0
  31. pyplumio/structures/boiler_power.py +3 -0
  32. pyplumio/structures/ecomax_parameters.py +7 -815
  33. pyplumio/structures/fan_power.py +3 -0
  34. pyplumio/structures/frame_versions.py +3 -0
  35. pyplumio/structures/fuel_consumption.py +3 -0
  36. pyplumio/structures/fuel_level.py +3 -0
  37. pyplumio/structures/lambda_sensor.py +8 -0
  38. pyplumio/structures/mixer_parameters.py +9 -245
  39. pyplumio/structures/mixer_sensors.py +9 -0
  40. pyplumio/structures/modules.py +14 -0
  41. pyplumio/structures/network_info.py +11 -0
  42. pyplumio/structures/output_flags.py +9 -0
  43. pyplumio/structures/outputs.py +21 -0
  44. pyplumio/structures/pending_alerts.py +3 -0
  45. pyplumio/structures/product_info.py +5 -2
  46. pyplumio/structures/program_version.py +3 -0
  47. pyplumio/structures/regulator_data.py +4 -1
  48. pyplumio/structures/regulator_data_schema.py +3 -0
  49. pyplumio/structures/schedules.py +18 -1
  50. pyplumio/structures/statuses.py +9 -0
  51. pyplumio/structures/temperatures.py +22 -0
  52. pyplumio/structures/thermostat_parameters.py +13 -199
  53. pyplumio/structures/thermostat_sensors.py +9 -0
  54. pyplumio/utils.py +14 -12
  55. {pyplumio-0.5.41.dist-info → pyplumio-0.5.43.dist-info}/METADATA +30 -17
  56. pyplumio-0.5.43.dist-info/RECORD +63 -0
  57. {pyplumio-0.5.41.dist-info → pyplumio-0.5.43.dist-info}/WHEEL +1 -1
  58. pyplumio-0.5.41.dist-info/RECORD +0 -60
  59. {pyplumio-0.5.41.dist-info → pyplumio-0.5.43.dist-info}/licenses/LICENSE +0 -0
  60. {pyplumio-0.5.41.dist-info → pyplumio-0.5.43.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.41'
21
- __version_tuple__ = version_tuple = (0, 5, 41)
20
+ __version__ = version = '0.5.43'
21
+ __version_tuple__ = version_tuple = (0, 5, 43)
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
@@ -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."""
@@ -8,12 +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, ATTR_LOADED, DeviceType, FrameType, State
12
12
  from pyplumio.exceptions import RequestError, UnknownDeviceError
13
+ from pyplumio.filters import on_change
13
14
  from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
14
- from pyplumio.helpers.event_manager import EventManager
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.parameters import NumericType, Parameter
17
18
  from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
18
19
  from pyplumio.structures.network_info import NetworkInfo
19
20
  from pyplumio.utils import to_camelcase
@@ -47,6 +48,8 @@ def get_device_handler(device_type: int) -> str:
47
48
  class Device(ABC, EventManager):
48
49
  """Represents a device."""
49
50
 
51
+ __slots__ = ("queue",)
52
+
50
53
  queue: asyncio.Queue[Frame]
51
54
 
52
55
  def __init__(self, queue: asyncio.Queue[Frame]) -> None:
@@ -123,6 +126,8 @@ class PhysicalDevice(Device, ABC):
123
126
  virtual devices associated with them via parent property.
124
127
  """
125
128
 
129
+ __slots__ = ("address", "_network", "_setup_frames", "_frame_versions")
130
+
126
131
  address: ClassVar[int]
127
132
  _network: NetworkInfo
128
133
  _setup_frames: tuple[DataFrameDescription, ...]
@@ -134,22 +139,19 @@ class PhysicalDevice(Device, ABC):
134
139
  self._network = network
135
140
  self._frame_versions = {}
136
141
 
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)
142
+ @event_listener(ATTR_FRAME_VERSIONS, on_change)
143
+ async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
144
+ """Check frame versions and update outdated frames."""
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
+ _LOGGER.debug("Updating frame %s to version %i", frame_type, version)
152
+ request = await Request.create(frame_type, recipient=self.address)
153
+ self.queue.put_nowait(request)
154
+ self._frame_versions[frame_type] = version
153
155
 
154
156
  def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
155
157
  """Return True if frame data is up to date, False otherwise."""
@@ -218,6 +220,8 @@ class PhysicalDevice(Device, ABC):
218
220
  class VirtualDevice(Device, ABC):
219
221
  """Represents a virtual device associated with physical device."""
220
222
 
223
+ __slots__ = ("parent", "index")
224
+
221
225
  parent: PhysicalDevice
222
226
  index: int
223
227
 
@@ -228,3 +232,12 @@ class VirtualDevice(Device, ABC):
228
232
  super().__init__(queue)
229
233
  self.parent = parent
230
234
  self.index = index
235
+
236
+
237
+ __all__ = [
238
+ "Device",
239
+ "PhysicalDevice",
240
+ "VirtualDevice",
241
+ "is_known_device_type",
242
+ "get_device_handler",
243
+ ]
@@ -12,27 +12,33 @@ from pyplumio.const import (
12
12
  ATTR_PASSWORD,
13
13
  ATTR_SENSORS,
14
14
  ATTR_STATE,
15
+ STATE_OFF,
16
+ STATE_ON,
15
17
  DeviceState,
16
18
  DeviceType,
17
19
  FrameType,
20
+ State,
18
21
  )
19
22
  from pyplumio.devices import PhysicalDevice
20
23
  from pyplumio.devices.mixer import Mixer
21
24
  from pyplumio.devices.thermostat import Thermostat
22
25
  from pyplumio.filters import on_change
23
26
  from pyplumio.frames import DataFrameDescription, Frame, Request
24
- from pyplumio.helpers.parameter import STATE_OFF, STATE_ON, ParameterValues, State
27
+ from pyplumio.helpers.event_manager import event_listener
25
28
  from pyplumio.helpers.schedule import Schedule, ScheduleDay
26
- from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
27
- from pyplumio.structures.ecomax_parameters import (
28
- ATTR_ECOMAX_CONTROL,
29
- ATTR_ECOMAX_PARAMETERS,
29
+ from pyplumio.parameters import ParameterValues
30
+ from pyplumio.parameters.ecomax import (
30
31
  ECOMAX_CONTROL_PARAMETER,
31
- ECOMAX_PARAMETERS,
32
32
  THERMOSTAT_PROFILE_PARAMETER,
33
33
  EcomaxNumber,
34
34
  EcomaxSwitch,
35
35
  EcomaxSwitchDescription,
36
+ get_ecomax_parameter_types,
37
+ )
38
+ from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
39
+ from pyplumio.structures.ecomax_parameters import (
40
+ ATTR_ECOMAX_CONTROL,
41
+ ATTR_ECOMAX_PARAMETERS,
36
42
  )
37
43
  from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
38
44
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
@@ -103,6 +109,8 @@ _LOGGER = logging.getLogger(__name__)
103
109
  class EcoMAX(PhysicalDevice):
104
110
  """Represents an ecoMAX controller."""
105
111
 
112
+ __slots__ = ("_fuel_burned_time_ns",)
113
+
106
114
  address = DeviceType.ECOMAX
107
115
  _setup_frames = SETUP_FRAME_TYPES
108
116
 
@@ -111,17 +119,6 @@ class EcoMAX(PhysicalDevice):
111
119
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
112
120
  """Initialize a new ecoMAX controller."""
113
121
  super().__init__(queue, network)
114
- self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
115
- self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_meter)
116
- self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
117
- self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
118
- self.subscribe(ATTR_SCHEDULES, self._add_schedules)
119
- self.subscribe(ATTR_SCHEDULE_PARAMETERS, self._add_schedule_parameters)
120
- self.subscribe(ATTR_SENSORS, self._handle_ecomax_sensors)
121
- self.subscribe(ATTR_STATE, on_change(self._add_ecomax_control_parameter))
122
- self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
123
- self.subscribe(ATTR_THERMOSTAT_PROFILE, self._add_thermostat_profile_parameter)
124
- self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
125
122
  self._fuel_burned_time_ns = time.perf_counter_ns()
126
123
 
127
124
  async def async_setup(self) -> bool:
@@ -165,21 +162,53 @@ class EcoMAX(PhysicalDevice):
165
162
 
166
163
  return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
167
164
 
168
- async def _handle_ecomax_parameters(
165
+ async def _set_ecomax_state(self, state: State) -> bool:
166
+ """Try to set the ecoMAX control state."""
167
+ try:
168
+ switch: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
169
+ return await switch.set(state)
170
+ except KeyError:
171
+ _LOGGER.error("ecoMAX control is not available. Please try again later.")
172
+
173
+ return False
174
+
175
+ async def turn_on(self) -> bool:
176
+ """Turn on the ecoMAX controller."""
177
+ return await self._set_ecomax_state(STATE_ON)
178
+
179
+ async def turn_off(self) -> bool:
180
+ """Turn off the ecoMAX controller."""
181
+ return await self._set_ecomax_state(STATE_OFF)
182
+
183
+ def turn_on_nowait(self) -> None:
184
+ """Turn on the ecoMAX controller without waiting."""
185
+ self.create_task(self.turn_on())
186
+
187
+ def turn_off_nowait(self) -> None:
188
+ """Turn off the ecoMAX controller without waiting."""
189
+ self.create_task(self.turn_off())
190
+
191
+ async def shutdown(self) -> None:
192
+ """Shutdown tasks for the ecoMAX controller and sub-devices."""
193
+ mixers: dict[str, Mixer] = self.get_nowait(ATTR_MIXERS, {})
194
+ thermostats: dict[str, Thermostat] = self.get_nowait(ATTR_THERMOSTATS, {})
195
+ devices = (mixers | thermostats).values()
196
+ await asyncio.gather(*(device.shutdown() for device in devices))
197
+ await super().shutdown()
198
+
199
+ @event_listener(ATTR_ECOMAX_PARAMETERS)
200
+ async def on_event_ecomax_parameters(
169
201
  self, parameters: Sequence[tuple[int, ParameterValues]]
170
202
  ) -> bool:
171
- """Handle ecoMAX parameters.
172
-
173
- For each parameter dispatch an event with the parameter's name
174
- and value.
175
- """
176
- product: ProductInfo = await self.get(ATTR_PRODUCT)
203
+ """Update ecoMAX parameters and dispatch the events."""
204
+ product_info: ProductInfo = await self.get(ATTR_PRODUCT)
177
205
 
178
206
  def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
179
207
  """Get dispatch calls for ecoMAX parameter events."""
208
+ parameter_types = get_ecomax_parameter_types(product_info)
180
209
  for index, values in parameters:
181
210
  try:
182
- description = ECOMAX_PARAMETERS[product.type][index]
211
+ description = parameter_types[index]
183
212
  except IndexError:
184
213
  _LOGGER.warning(
185
214
  "Encountered unknown ecoMAX parameter (%i): %s. "
@@ -188,7 +217,7 @@ class EcoMAX(PhysicalDevice):
188
217
  "and open a feature request to support %s",
189
218
  index,
190
219
  values,
191
- product.model,
220
+ product_info.model,
192
221
  )
193
222
  return
194
223
 
@@ -207,8 +236,9 @@ class EcoMAX(PhysicalDevice):
207
236
  await asyncio.gather(*_ecomax_parameter_events())
208
237
  return True
209
238
 
210
- async def _add_burned_fuel_meter(self, fuel_consumption: float) -> None:
211
- """Calculate and dispatch the amount of fuel burned.
239
+ @event_listener(ATTR_FUEL_CONSUMPTION)
240
+ async def on_event_fuel_consumption(self, fuel_consumption: float) -> None:
241
+ """Update the amount of burned fuel.
212
242
 
213
243
  This method calculates the fuel burned based on the time
214
244
  elapsed since the last sensor message, which contains fuel
@@ -231,16 +261,12 @@ class EcoMAX(PhysicalDevice):
231
261
  nanoseconds_passed / NANOSECONDS_IN_SECOND,
232
262
  )
233
263
 
234
- async def _handle_mixer_parameters(
264
+ @event_listener(ATTR_MIXER_PARAMETERS)
265
+ async def on_event_mixer_parameters(
235
266
  self,
236
267
  parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
237
268
  ) -> bool:
238
- """Handle mixer parameters.
239
-
240
- For each parameter dispatch an event with the
241
- parameter's name and value. Events are dispatched for the
242
- respective mixer instance.
243
- """
269
+ """Handle mixer parameters and dispatch the events."""
244
270
  if parameters:
245
271
  await asyncio.gather(
246
272
  *(
@@ -252,15 +278,11 @@ class EcoMAX(PhysicalDevice):
252
278
 
253
279
  return False
254
280
 
255
- async def _handle_mixer_sensors(
281
+ @event_listener(ATTR_MIXER_SENSORS)
282
+ async def on_event_mixer_sensors(
256
283
  self, sensors: dict[int, dict[str, Any]] | None
257
284
  ) -> bool:
258
- """Handle mixer sensors.
259
-
260
- For each sensor dispatch an event with the
261
- sensor's name and value. Events are dispatched for the
262
- respective mixer instance.
263
- """
285
+ """Update mixer sensors and dispatch the events."""
264
286
  if sensors:
265
287
  await asyncio.gather(
266
288
  *(
@@ -272,29 +294,11 @@ class EcoMAX(PhysicalDevice):
272
294
 
273
295
  return False
274
296
 
275
- async def _add_schedules(
276
- self, schedules: list[tuple[int, list[list[bool]]]]
277
- ) -> dict[str, Schedule]:
278
- """Add schedules to the dataset."""
279
- return {
280
- SCHEDULES[index]: Schedule(
281
- name=SCHEDULES[index],
282
- device=self,
283
- monday=ScheduleDay.from_iterable(schedule[1]),
284
- tuesday=ScheduleDay.from_iterable(schedule[2]),
285
- wednesday=ScheduleDay.from_iterable(schedule[3]),
286
- thursday=ScheduleDay.from_iterable(schedule[4]),
287
- friday=ScheduleDay.from_iterable(schedule[5]),
288
- saturday=ScheduleDay.from_iterable(schedule[6]),
289
- sunday=ScheduleDay.from_iterable(schedule[0]),
290
- )
291
- for index, schedule in schedules
292
- }
293
-
294
- async def _add_schedule_parameters(
297
+ @event_listener(ATTR_SCHEDULE_PARAMETERS)
298
+ async def on_event_schedule_parameters(
295
299
  self, parameters: Sequence[tuple[int, ParameterValues]]
296
300
  ) -> bool:
297
- """Add schedule parameters to the dataset."""
301
+ """Update schedule parameters and dispatch the events."""
298
302
 
299
303
  def _schedule_parameter_events() -> Generator[Coroutine, Any, None]:
300
304
  """Get dispatch calls for schedule parameter events."""
@@ -315,40 +319,20 @@ class EcoMAX(PhysicalDevice):
315
319
  await asyncio.gather(*_schedule_parameter_events())
316
320
  return True
317
321
 
318
- async def _handle_ecomax_sensors(self, sensors: dict[str, Any]) -> bool:
319
- """Handle ecoMAX sensors.
320
-
321
- For each sensor dispatch an event with the sensor's name and
322
- value.
323
- """
322
+ @event_listener(ATTR_SENSORS)
323
+ async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
324
+ """Update ecoMAX sensors and dispatch the events."""
324
325
  await asyncio.gather(
325
326
  *(self.dispatch(name, value) for name, value in sensors.items())
326
327
  )
327
328
  return True
328
329
 
329
- async def _add_ecomax_control_parameter(self, mode: DeviceState) -> None:
330
- """Create ecoMAX control parameter instance and dispatch an event."""
331
- await self.dispatch(
332
- ECOMAX_CONTROL_PARAMETER.name,
333
- EcomaxSwitch.create_or_update(
334
- description=ECOMAX_CONTROL_PARAMETER,
335
- device=self,
336
- values=ParameterValues(
337
- value=int(mode != DeviceState.OFF), min_value=0, max_value=1
338
- ),
339
- ),
340
- )
341
-
342
- async def _handle_thermostat_parameters(
330
+ @event_listener(ATTR_THERMOSTAT_PARAMETERS)
331
+ async def on_event_thermostat_parameters(
343
332
  self,
344
333
  parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
345
334
  ) -> bool:
346
- """Handle thermostat parameters.
347
-
348
- For each parameter dispatch an event with the
349
- parameter's name and value. Events are dispatched for the
350
- respective thermostat instance.
351
- """
335
+ """Handle thermostat parameters and dispatch the events."""
352
336
  if parameters:
353
337
  await asyncio.gather(
354
338
  *(
@@ -362,10 +346,11 @@ class EcoMAX(PhysicalDevice):
362
346
 
363
347
  return False
364
348
 
365
- async def _add_thermostat_profile_parameter(
349
+ @event_listener(ATTR_THERMOSTAT_PROFILE)
350
+ async def on_event_thermostat_profile(
366
351
  self, values: ParameterValues | None
367
352
  ) -> EcomaxNumber | None:
368
- """Add thermostat profile parameter to the dataset."""
353
+ """Update thermostat profile parameter."""
369
354
  if values:
370
355
  return EcomaxNumber(
371
356
  device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
@@ -373,15 +358,11 @@ class EcoMAX(PhysicalDevice):
373
358
 
374
359
  return None
375
360
 
376
- async def _handle_thermostat_sensors(
361
+ @event_listener(ATTR_THERMOSTAT_SENSORS)
362
+ async def on_event_thermostat_sensors(
377
363
  self, sensors: dict[int, dict[str, Any]] | None
378
364
  ) -> bool:
379
- """Handle thermostat sensors.
380
-
381
- For each sensor dispatch an event with the
382
- sensor's name and value. Events are dispatched for the
383
- respective thermostat instance.
384
- """
365
+ """Update thermostat sensors and dispatch the events."""
385
366
  if sensors:
386
367
  await asyncio.gather(
387
368
  *(
@@ -396,36 +377,39 @@ class EcoMAX(PhysicalDevice):
396
377
 
397
378
  return False
398
379
 
399
- async def _set_ecomax_state(self, state: State) -> bool:
400
- """Try to set the ecoMAX control state."""
401
- try:
402
- switch: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
403
- return await switch.set(state)
404
- except KeyError:
405
- _LOGGER.error("ecoMAX control is not available. Please try again later.")
406
-
407
- return False
408
-
409
- async def turn_on(self) -> bool:
410
- """Turn on the ecoMAX controller."""
411
- return await self._set_ecomax_state(STATE_ON)
412
-
413
- async def turn_off(self) -> bool:
414
- """Turn off the ecoMAX controller."""
415
- return await self._set_ecomax_state(STATE_OFF)
380
+ @event_listener(ATTR_SCHEDULES)
381
+ async def on_event_schedules(
382
+ self, schedules: list[tuple[int, list[list[bool]]]]
383
+ ) -> dict[str, Schedule]:
384
+ """Update schedules."""
385
+ return {
386
+ SCHEDULES[index]: Schedule(
387
+ name=SCHEDULES[index],
388
+ device=self,
389
+ monday=ScheduleDay.from_iterable(schedule[1]),
390
+ tuesday=ScheduleDay.from_iterable(schedule[2]),
391
+ wednesday=ScheduleDay.from_iterable(schedule[3]),
392
+ thursday=ScheduleDay.from_iterable(schedule[4]),
393
+ friday=ScheduleDay.from_iterable(schedule[5]),
394
+ saturday=ScheduleDay.from_iterable(schedule[6]),
395
+ sunday=ScheduleDay.from_iterable(schedule[0]),
396
+ )
397
+ for index, schedule in schedules
398
+ }
416
399
 
417
- def turn_on_nowait(self) -> None:
418
- """Turn on the ecoMAX controller without waiting."""
419
- self.create_task(self.turn_on())
400
+ @event_listener(ATTR_STATE, on_change)
401
+ async def on_event_state(self, state: DeviceState) -> None:
402
+ """Update the ecoMAX control parameter."""
403
+ await self.dispatch(
404
+ ECOMAX_CONTROL_PARAMETER.name,
405
+ EcomaxSwitch.create_or_update(
406
+ description=ECOMAX_CONTROL_PARAMETER,
407
+ device=self,
408
+ values=ParameterValues(
409
+ value=int(state != DeviceState.OFF), min_value=0, max_value=1
410
+ ),
411
+ ),
412
+ )
420
413
 
421
- def turn_off_nowait(self) -> None:
422
- """Turn off the ecoMAX controller without waiting."""
423
- self.create_task(self.turn_off())
424
414
 
425
- async def shutdown(self) -> None:
426
- """Shutdown tasks for the ecoMAX controller and sub-devices."""
427
- mixers: dict[str, Mixer] = self.get_nowait(ATTR_MIXERS, {})
428
- thermostats: dict[str, Thermostat] = self.get_nowait(ATTR_THERMOSTATS, {})
429
- devices = (mixers | thermostats).values()
430
- await asyncio.gather(*(device.shutdown() for device in devices))
431
- await super().shutdown()
415
+ __all__ = ["ATTR_MIXERS", "ATTR_THERMOSTATS", "ATTR_FUEL_BURNED", "EcoMAX"]
@@ -9,4 +9,9 @@ from pyplumio.devices import PhysicalDevice
9
9
  class EcoSTER(PhysicalDevice):
10
10
  """Represents an ecoSTER thermostat."""
11
11
 
12
+ __slots__ = ()
13
+
12
14
  address = DeviceType.ECOSTER
15
+
16
+
17
+ __all__ = ["EcoSTER"]
pyplumio/devices/mixer.py CHANGED
@@ -5,63 +5,50 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from collections.abc import Coroutine, Generator, Sequence
7
7
  import logging
8
- from typing import TYPE_CHECKING, Any
8
+ from typing import Any
9
9
 
10
- from pyplumio.devices import PhysicalDevice, VirtualDevice
11
- from pyplumio.helpers.parameter import ParameterValues
12
- from pyplumio.structures.mixer_parameters import (
13
- ATTR_MIXER_PARAMETERS,
14
- MIXER_PARAMETERS,
10
+ from pyplumio.devices import VirtualDevice
11
+ from pyplumio.helpers.event_manager import event_listener
12
+ from pyplumio.parameters import ParameterValues
13
+ from pyplumio.parameters.mixer import (
15
14
  MixerNumber,
16
15
  MixerSwitch,
17
16
  MixerSwitchDescription,
17
+ get_mixer_parameter_types,
18
18
  )
19
+ from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
19
20
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
20
21
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
21
22
 
22
- if TYPE_CHECKING:
23
- from pyplumio.frames import Frame
24
-
25
23
  _LOGGER = logging.getLogger(__name__)
26
24
 
27
25
 
28
26
  class Mixer(VirtualDevice):
29
27
  """Represents a mixer."""
30
28
 
31
- def __init__(
32
- self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
33
- ) -> None:
34
- """Initialize a new mixer."""
35
- super().__init__(queue, parent, index)
36
- self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
37
- self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
38
-
39
- async def _handle_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
40
- """Handle mixer sensors.
29
+ __slots__ = ()
41
30
 
42
- For each sensor dispatch an event with the
43
- sensor's name and value.
44
- """
31
+ @event_listener(ATTR_MIXER_SENSORS)
32
+ async def on_event_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
33
+ """Update mixer sensors and dispatch the events."""
45
34
  await asyncio.gather(
46
35
  *(self.dispatch(name, value) for name, value in sensors.items())
47
36
  )
48
37
  return True
49
38
 
50
- async def _handle_mixer_parameters(
39
+ @event_listener(ATTR_MIXER_PARAMETERS)
40
+ async def on_event_mixer_parameters(
51
41
  self, parameters: Sequence[tuple[int, ParameterValues]]
52
42
  ) -> bool:
53
- """Handle mixer parameters.
54
-
55
- For each parameter dispatch an event with the
56
- parameter's name and value.
57
- """
58
- product: ProductInfo = await self.parent.get(ATTR_PRODUCT)
43
+ """Update mixer parameters and dispatch the events."""
44
+ product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
59
45
 
60
46
  def _mixer_parameter_events() -> Generator[Coroutine, Any, None]:
61
47
  """Get dispatch calls for mixer parameter events."""
48
+ parameter_types = get_mixer_parameter_types(product_info)
62
49
  for index, values in parameters:
63
50
  try:
64
- description = MIXER_PARAMETERS[product.type][index]
51
+ description = parameter_types[index]
65
52
  except IndexError:
66
53
  _LOGGER.warning(
67
54
  "Encountered unknown mixer parameter (%i): %s. "
@@ -70,7 +57,7 @@ class Mixer(VirtualDevice):
70
57
  "and open a feature request to support %s",
71
58
  index,
72
59
  values,
73
- product.model,
60
+ product_info.model,
74
61
  )
75
62
  return
76
63
 
@@ -88,3 +75,6 @@ class Mixer(VirtualDevice):
88
75
 
89
76
  await asyncio.gather(*_mixer_parameter_events())
90
77
  return True
78
+
79
+
80
+ __all__ = ["Mixer"]