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
@@ -3,45 +3,50 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Iterable, Sequence
6
+ from collections.abc import Coroutine, Generator, Iterable
7
7
  import logging
8
8
  import time
9
9
  from typing import Any, Final
10
10
 
11
11
  from pyplumio.const import (
12
+ ATTR_FRAME_ERRORS,
12
13
  ATTR_PASSWORD,
13
14
  ATTR_SENSORS,
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
25
+ from pyplumio.exceptions import RequestError
22
26
  from pyplumio.filters import on_change
23
27
  from pyplumio.frames import DataFrameDescription, Frame, Request
24
- from pyplumio.helpers.parameter import STATE_OFF, STATE_ON, ParameterValues, State
28
+ from pyplumio.helpers.event_manager import event_listener
25
29
  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,
30
+ from pyplumio.parameters import ParameterValues
31
+ from pyplumio.parameters.ecomax import (
30
32
  ECOMAX_CONTROL_PARAMETER,
31
- ECOMAX_PARAMETERS,
32
33
  THERMOSTAT_PROFILE_PARAMETER,
33
34
  EcomaxNumber,
34
35
  EcomaxSwitch,
35
36
  EcomaxSwitchDescription,
37
+ get_ecomax_parameter_types,
38
+ )
39
+ from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
40
+ from pyplumio.structures.ecomax_parameters import (
41
+ ATTR_ECOMAX_CONTROL,
42
+ ATTR_ECOMAX_PARAMETERS,
36
43
  )
37
- from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
38
44
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
39
45
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
40
46
  from pyplumio.structures.network_info import ATTR_NETWORK, NetworkInfo
41
47
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
42
48
  from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
43
49
  from pyplumio.structures.schedules import (
44
- ATTR_SCHEDULE_PARAMETERS,
45
50
  ATTR_SCHEDULES,
46
51
  SCHEDULE_PARAMETERS,
47
52
  SCHEDULES,
@@ -49,20 +54,53 @@ from pyplumio.structures.schedules import (
49
54
  ScheduleSwitch,
50
55
  ScheduleSwitchDescription,
51
56
  )
52
- from pyplumio.structures.thermostat_parameters import (
53
- ATTR_THERMOSTAT_PARAMETERS,
54
- ATTR_THERMOSTAT_PROFILE,
55
- )
57
+ from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
56
58
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
57
59
 
60
+ _LOGGER = logging.getLogger(__name__)
61
+
62
+
58
63
  ATTR_MIXERS: Final = "mixers"
59
64
  ATTR_THERMOSTATS: Final = "thermostats"
60
65
  ATTR_FUEL_BURNED: Final = "fuel_burned"
61
66
 
62
- NANOSECONDS_IN_SECOND: Final = 1_000_000_000
63
- MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
67
+ MAX_TIME_SINCE_LAST_FUEL_UPDATE: Final = 5 * 60
68
+
69
+
70
+ class FuelMeter:
71
+ """Represents a fuel meter.
72
+
73
+ Calculates the fuel burned based on the time
74
+ elapsed since the last sensor message, which contains fuel
75
+ consumption data. If the elapsed time is within the acceptable
76
+ range, it returns the fuel burned data. Otherwise, it logs a
77
+ warning and returns None.
78
+ """
79
+
80
+ __slots__ = ("_last_update_time",)
64
81
 
65
- SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
82
+ _last_update_time: float
83
+
84
+ def __init__(self) -> None:
85
+ """Initialize a new fuel meter."""
86
+ self._last_update_time = time.monotonic()
87
+
88
+ def calculate(self, fuel_consumption: float) -> float | None:
89
+ """Calculate the amount of burned fuel since last update."""
90
+ current_time = time.monotonic()
91
+ time_since_update = current_time - self._last_update_time
92
+ self._last_update_time = current_time
93
+ if time_since_update < MAX_TIME_SINCE_LAST_FUEL_UPDATE:
94
+ return fuel_consumption * (time_since_update / 3600)
95
+
96
+ _LOGGER.warning(
97
+ "Skipping outdated fuel consumption data (was %f seconds old)",
98
+ time_since_update,
99
+ )
100
+ return None
101
+
102
+
103
+ REQUIRED: tuple[DataFrameDescription, ...] = (
66
104
  DataFrameDescription(
67
105
  frame_type=FrameType.REQUEST_UID,
68
106
  provides=ATTR_PRODUCT,
@@ -97,37 +135,22 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
97
135
  ),
98
136
  )
99
137
 
100
- _LOGGER = logging.getLogger(__name__)
138
+ REQUIRED_TYPES = [description.frame_type for description in REQUIRED]
101
139
 
102
140
 
103
141
  class EcoMAX(PhysicalDevice):
104
142
  """Represents an ecoMAX controller."""
105
143
 
106
- address = DeviceType.ECOMAX
107
- _setup_frames = SETUP_FRAME_TYPES
144
+ __slots__ = ("_fuel_meter",)
108
145
 
109
- _fuel_burned_time_ns: int
146
+ _fuel_meter: FuelMeter
147
+
148
+ address = DeviceType.ECOMAX
110
149
 
111
150
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
112
151
  """Initialize a new ecoMAX controller."""
113
152
  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
- self._fuel_burned_time_ns = time.perf_counter_ns()
126
-
127
- async def async_setup(self) -> bool:
128
- """Set up an ecoMAX controller."""
129
- await self.wait_for(ATTR_SENSORS)
130
- return await super().async_setup()
153
+ self._fuel_meter = FuelMeter()
131
154
 
132
155
  def handle_frame(self, frame: Frame) -> None:
133
156
  """Handle frame received from the ecoMAX device."""
@@ -165,21 +188,83 @@ class EcoMAX(PhysicalDevice):
165
188
 
166
189
  return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
167
190
 
168
- async def _handle_ecomax_parameters(
169
- self, parameters: Sequence[tuple[int, ParameterValues]]
170
- ) -> bool:
171
- """Handle ecoMAX parameters.
191
+ async def _request_frame_version(
192
+ self, frame_type: FrameType | int, version: int
193
+ ) -> None:
194
+ """Request frame version from the device."""
195
+ if frame_type not in REQUIRED_TYPES:
196
+ await super()._request_frame_version(frame_type, version)
172
197
 
173
- For each parameter dispatch an event with the parameter's name
174
- and value.
175
- """
176
- product: ProductInfo = await self.get(ATTR_PRODUCT)
198
+ async def _set_ecomax_state(self, state: State) -> bool:
199
+ """Try to set the ecoMAX control state."""
200
+ try:
201
+ switch: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
202
+ return await switch.set(state)
203
+ except KeyError:
204
+ _LOGGER.error("ecoMAX control is not available. Please try again later.")
205
+
206
+ return False
207
+
208
+ async def turn_on(self) -> bool:
209
+ """Turn on the ecoMAX controller."""
210
+ return await self._set_ecomax_state(STATE_ON)
211
+
212
+ async def turn_off(self) -> bool:
213
+ """Turn off the ecoMAX controller."""
214
+ return await self._set_ecomax_state(STATE_OFF)
215
+
216
+ def turn_on_nowait(self) -> None:
217
+ """Turn on the ecoMAX controller without waiting."""
218
+ self.create_task(self.turn_on())
219
+
220
+ def turn_off_nowait(self) -> None:
221
+ """Turn off the ecoMAX controller without waiting."""
222
+ self.create_task(self.turn_off())
223
+
224
+ async def shutdown(self) -> None:
225
+ """Shutdown tasks for the ecoMAX controller and sub-devices."""
226
+ mixers: dict[str, Mixer] = self.get_nowait(ATTR_MIXERS, {})
227
+ thermostats: dict[str, Thermostat] = self.get_nowait(ATTR_THERMOSTATS, {})
228
+ devices = (mixers | thermostats).values()
229
+ await asyncio.gather(*(device.shutdown() for device in devices))
230
+ await super().shutdown()
231
+
232
+ @event_listener
233
+ async def on_event_setup(self, setup: bool) -> None:
234
+ """Request frames required to set up an ecoMAX entry."""
235
+ _LOGGER.info("Setting up device entry")
236
+ await self.wait_for(ATTR_SENSORS)
237
+ results = await asyncio.gather(
238
+ *(
239
+ self.request(description.provides, description.frame_type)
240
+ for description in REQUIRED
241
+ ),
242
+ return_exceptions=True,
243
+ )
244
+
245
+ errors = [
246
+ result.frame_type for result in results if isinstance(result, RequestError)
247
+ ]
248
+
249
+ if errors:
250
+ self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
251
+
252
+ _LOGGER.info("Device entry setup done")
253
+
254
+ @event_listener
255
+ async def on_event_ecomax_parameters(
256
+ self, parameters: list[tuple[int, ParameterValues]]
257
+ ) -> bool:
258
+ """Update ecoMAX parameters and dispatch the events."""
259
+ _LOGGER.info("Received device parameters")
260
+ product_info: ProductInfo = await self.get(ATTR_PRODUCT)
177
261
 
178
262
  def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
179
263
  """Get dispatch calls for ecoMAX parameter events."""
264
+ parameter_types = get_ecomax_parameter_types(product_info)
180
265
  for index, values in parameters:
181
266
  try:
182
- description = ECOMAX_PARAMETERS[product.type][index]
267
+ description = parameter_types[index]
183
268
  except IndexError:
184
269
  _LOGGER.warning(
185
270
  "Encountered unknown ecoMAX parameter (%i): %s. "
@@ -188,7 +273,7 @@ class EcoMAX(PhysicalDevice):
188
273
  "and open a feature request to support %s",
189
274
  index,
190
275
  values,
191
- product.model,
276
+ product_info.model,
192
277
  )
193
278
  return
194
279
 
@@ -207,40 +292,20 @@ class EcoMAX(PhysicalDevice):
207
292
  await asyncio.gather(*_ecomax_parameter_events())
208
293
  return True
209
294
 
210
- async def _add_burned_fuel_meter(self, fuel_consumption: float) -> None:
211
- """Calculate and dispatch the amount of fuel burned.
295
+ @event_listener
296
+ async def on_event_fuel_consumption(self, fuel_consumption: float) -> None:
297
+ """Update the amount of burned fuel."""
298
+ fuel_burned = self._fuel_meter.calculate(fuel_consumption)
299
+ if fuel_burned is not None:
300
+ self.dispatch_nowait(ATTR_FUEL_BURNED, fuel_burned)
212
301
 
213
- This method calculates the fuel burned based on the time
214
- elapsed since the last sensor message, which contains fuel
215
- consumption data. If the elapsed time is within the acceptable
216
- range, it dispatches the fuel burned data. Otherwise, it logs a
217
- warning and skips the outdated data.
218
- """
219
- time_ns = time.perf_counter_ns()
220
- nanoseconds_passed = time_ns - self._fuel_burned_time_ns
221
- self._fuel_burned_time_ns = time_ns
222
- if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
223
- return self.dispatch_nowait(
224
- ATTR_FUEL_BURNED,
225
- fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
226
- )
227
-
228
- _LOGGER.warning(
229
- "Skipping outdated fuel consumption data: %f (was %i seconds old)",
230
- fuel_consumption,
231
- nanoseconds_passed / NANOSECONDS_IN_SECOND,
232
- )
233
-
234
- async def _handle_mixer_parameters(
302
+ @event_listener
303
+ async def on_event_mixer_parameters(
235
304
  self,
236
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
305
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
237
306
  ) -> 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
- """
307
+ """Handle mixer parameters and dispatch the events."""
308
+ _LOGGER.info("Received mixer parameters")
244
309
  if parameters:
245
310
  await asyncio.gather(
246
311
  *(
@@ -252,15 +317,12 @@ class EcoMAX(PhysicalDevice):
252
317
 
253
318
  return False
254
319
 
255
- async def _handle_mixer_sensors(
320
+ @event_listener
321
+ async def on_event_mixer_sensors(
256
322
  self, sensors: dict[int, dict[str, Any]] | None
257
323
  ) -> 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
- """
324
+ """Update mixer sensors and dispatch the events."""
325
+ _LOGGER.info("Received mixer sensors")
264
326
  if sensors:
265
327
  await asyncio.gather(
266
328
  *(
@@ -272,29 +334,11 @@ class EcoMAX(PhysicalDevice):
272
334
 
273
335
  return False
274
336
 
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(
295
- self, parameters: Sequence[tuple[int, ParameterValues]]
337
+ @event_listener
338
+ async def on_event_schedule_parameters(
339
+ self, parameters: list[tuple[int, ParameterValues]]
296
340
  ) -> bool:
297
- """Add schedule parameters to the dataset."""
341
+ """Update schedule parameters and dispatch the events."""
298
342
 
299
343
  def _schedule_parameter_events() -> Generator[Coroutine, Any, None]:
300
344
  """Get dispatch calls for schedule parameter events."""
@@ -315,40 +359,22 @@ class EcoMAX(PhysicalDevice):
315
359
  await asyncio.gather(*_schedule_parameter_events())
316
360
  return True
317
361
 
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
- """
362
+ @event_listener
363
+ async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
364
+ """Update ecoMAX sensors and dispatch the events."""
365
+ _LOGGER.info("Received device sensors")
324
366
  await asyncio.gather(
325
367
  *(self.dispatch(name, value) for name, value in sensors.items())
326
368
  )
327
369
  return True
328
370
 
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(
371
+ @event_listener
372
+ async def on_event_thermostat_parameters(
343
373
  self,
344
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
374
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
345
375
  ) -> 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
- """
376
+ """Handle thermostat parameters and dispatch the events."""
377
+ _LOGGER.info("Received thermostat parameters")
352
378
  if parameters:
353
379
  await asyncio.gather(
354
380
  *(
@@ -362,10 +388,11 @@ class EcoMAX(PhysicalDevice):
362
388
 
363
389
  return False
364
390
 
365
- async def _add_thermostat_profile_parameter(
391
+ @event_listener
392
+ async def on_event_thermostat_profile(
366
393
  self, values: ParameterValues | None
367
394
  ) -> EcomaxNumber | None:
368
- """Add thermostat profile parameter to the dataset."""
395
+ """Update thermostat profile parameter."""
369
396
  if values:
370
397
  return EcomaxNumber(
371
398
  device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
@@ -373,15 +400,12 @@ class EcoMAX(PhysicalDevice):
373
400
 
374
401
  return None
375
402
 
376
- async def _handle_thermostat_sensors(
403
+ @event_listener
404
+ async def on_event_thermostat_sensors(
377
405
  self, sensors: dict[int, dict[str, Any]] | None
378
406
  ) -> 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
- """
407
+ """Update thermostat sensors and dispatch the events."""
408
+ _LOGGER.info("Received thermostat sensors")
385
409
  if sensors:
386
410
  await asyncio.gather(
387
411
  *(
@@ -396,36 +420,40 @@ class EcoMAX(PhysicalDevice):
396
420
 
397
421
  return False
398
422
 
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)
423
+ @event_listener
424
+ async def on_event_schedules(
425
+ self, schedules: list[tuple[int, list[list[bool]]]]
426
+ ) -> dict[str, Schedule]:
427
+ """Update schedules."""
428
+ _LOGGER.info("Received device schedules")
429
+ return {
430
+ SCHEDULES[index]: Schedule(
431
+ name=SCHEDULES[index],
432
+ device=self,
433
+ monday=ScheduleDay.from_iterable(schedule[1]),
434
+ tuesday=ScheduleDay.from_iterable(schedule[2]),
435
+ wednesday=ScheduleDay.from_iterable(schedule[3]),
436
+ thursday=ScheduleDay.from_iterable(schedule[4]),
437
+ friday=ScheduleDay.from_iterable(schedule[5]),
438
+ saturday=ScheduleDay.from_iterable(schedule[6]),
439
+ sunday=ScheduleDay.from_iterable(schedule[0]),
440
+ )
441
+ for index, schedule in schedules
442
+ }
416
443
 
417
- def turn_on_nowait(self) -> None:
418
- """Turn on the ecoMAX controller without waiting."""
419
- self.create_task(self.turn_on())
444
+ @event_listener(filter=on_change)
445
+ async def on_event_state(self, state: DeviceState) -> None:
446
+ """Update the ecoMAX control parameter."""
447
+ await self.dispatch(
448
+ ECOMAX_CONTROL_PARAMETER.name,
449
+ EcomaxSwitch.create_or_update(
450
+ description=ECOMAX_CONTROL_PARAMETER,
451
+ device=self,
452
+ values=ParameterValues(
453
+ value=int(state != DeviceState.OFF), min_value=0, max_value=1
454
+ ),
455
+ ),
456
+ )
420
457
 
421
- def turn_off_nowait(self) -> None:
422
- """Turn off the ecoMAX controller without waiting."""
423
- self.create_task(self.turn_off())
424
458
 
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()
459
+ __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
@@ -3,65 +3,52 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Sequence
6
+ from collections.abc import Coroutine, Generator
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_sensors import ATTR_MIXER_SENSORS
20
19
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
21
20
 
22
- if TYPE_CHECKING:
23
- from pyplumio.frames import Frame
24
-
25
21
  _LOGGER = logging.getLogger(__name__)
26
22
 
27
23
 
28
24
  class Mixer(VirtualDevice):
29
25
  """Represents a mixer."""
30
26
 
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.
27
+ __slots__ = ()
41
28
 
42
- For each sensor dispatch an event with the
43
- sensor's name and value.
44
- """
29
+ @event_listener
30
+ async def on_event_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
31
+ """Update mixer sensors and dispatch the events."""
32
+ _LOGGER.info("Received mixer %i sensors", self.index)
45
33
  await asyncio.gather(
46
34
  *(self.dispatch(name, value) for name, value in sensors.items())
47
35
  )
48
36
  return True
49
37
 
50
- async def _handle_mixer_parameters(
51
- self, parameters: Sequence[tuple[int, ParameterValues]]
38
+ @event_listener
39
+ async def on_event_mixer_parameters(
40
+ self, parameters: list[tuple[int, ParameterValues]]
52
41
  ) -> 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)
42
+ """Update mixer parameters and dispatch the events."""
43
+ _LOGGER.info("Received mixer %i parameters", self.index)
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"]