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.
- pyplumio/__init__.py +3 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +14 -14
- pyplumio/const.py +8 -3
- pyplumio/{helpers/data_types.py → data_types.py} +23 -21
- pyplumio/devices/__init__.py +42 -41
- pyplumio/devices/ecomax.py +202 -174
- pyplumio/devices/ecoster.py +5 -0
- pyplumio/devices/mixer.py +24 -34
- pyplumio/devices/thermostat.py +24 -31
- pyplumio/filters.py +188 -147
- pyplumio/frames/__init__.py +20 -8
- pyplumio/frames/messages.py +3 -0
- pyplumio/frames/requests.py +21 -0
- pyplumio/frames/responses.py +18 -0
- pyplumio/helpers/async_cache.py +48 -0
- pyplumio/helpers/event_manager.py +58 -3
- pyplumio/helpers/factory.py +5 -2
- pyplumio/helpers/schedule.py +8 -5
- pyplumio/helpers/task_manager.py +3 -0
- pyplumio/helpers/timeout.py +7 -6
- pyplumio/helpers/uid.py +8 -5
- pyplumio/{helpers/parameter.py → parameters/__init__.py} +105 -5
- pyplumio/parameters/ecomax.py +868 -0
- pyplumio/parameters/mixer.py +245 -0
- pyplumio/parameters/thermostat.py +197 -0
- pyplumio/protocol.py +21 -10
- pyplumio/stream.py +3 -0
- pyplumio/structures/__init__.py +3 -0
- pyplumio/structures/alerts.py +9 -6
- pyplumio/structures/boiler_load.py +3 -0
- pyplumio/structures/boiler_power.py +4 -1
- pyplumio/structures/ecomax_parameters.py +6 -800
- pyplumio/structures/fan_power.py +4 -1
- pyplumio/structures/frame_versions.py +4 -1
- pyplumio/structures/fuel_consumption.py +4 -1
- pyplumio/structures/fuel_level.py +3 -0
- pyplumio/structures/lambda_sensor.py +9 -1
- pyplumio/structures/mixer_parameters.py +8 -230
- pyplumio/structures/mixer_sensors.py +10 -1
- pyplumio/structures/modules.py +14 -0
- pyplumio/structures/network_info.py +12 -1
- pyplumio/structures/output_flags.py +10 -1
- pyplumio/structures/outputs.py +22 -1
- pyplumio/structures/pending_alerts.py +3 -0
- pyplumio/structures/product_info.py +6 -3
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +5 -2
- pyplumio/structures/regulator_data_schema.py +4 -1
- pyplumio/structures/schedules.py +18 -1
- pyplumio/structures/statuses.py +9 -0
- pyplumio/structures/temperatures.py +23 -1
- pyplumio/structures/thermostat_parameters.py +18 -184
- pyplumio/structures/thermostat_sensors.py +10 -1
- pyplumio/utils.py +14 -12
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/METADATA +32 -17
- pyplumio-0.5.44.dist-info/RECORD +64 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
- pyplumio-0.5.42.dist-info/RECORD +0 -60
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/top_level.txt +0 -0
pyplumio/devices/ecomax.py
CHANGED
@@ -3,45 +3,50 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Coroutine, Generator, Iterable
|
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
|
-
|
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.
|
28
|
+
from pyplumio.helpers.event_manager import event_listener
|
25
29
|
from pyplumio.helpers.schedule import Schedule, ScheduleDay
|
26
|
-
from pyplumio.
|
27
|
-
from pyplumio.
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
107
|
-
_setup_frames = SETUP_FRAME_TYPES
|
144
|
+
__slots__ = ("_fuel_meter",)
|
108
145
|
|
109
|
-
|
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.
|
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
|
169
|
-
self,
|
170
|
-
) ->
|
171
|
-
"""
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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 =
|
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
|
-
|
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
|
-
|
211
|
-
|
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
|
-
|
214
|
-
|
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,
|
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
|
-
|
320
|
+
@event_listener
|
321
|
+
async def on_event_mixer_sensors(
|
256
322
|
self, sensors: dict[int, dict[str, Any]] | None
|
257
323
|
) -> bool:
|
258
|
-
"""
|
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
|
-
|
276
|
-
|
277
|
-
|
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
|
-
"""
|
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
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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
|
-
|
330
|
-
|
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,
|
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
|
-
|
391
|
+
@event_listener
|
392
|
+
async def on_event_thermostat_profile(
|
366
393
|
self, values: ParameterValues | None
|
367
394
|
) -> EcomaxNumber | None:
|
368
|
-
"""
|
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
|
-
|
403
|
+
@event_listener
|
404
|
+
async def on_event_thermostat_sensors(
|
377
405
|
self, sensors: dict[int, dict[str, Any]] | None
|
378
406
|
) -> bool:
|
379
|
-
"""
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
-
|
418
|
-
|
419
|
-
|
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
|
-
|
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"]
|
pyplumio/devices/ecoster.py
CHANGED
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
|
6
|
+
from collections.abc import Coroutine, Generator
|
7
7
|
import logging
|
8
|
-
from typing import
|
8
|
+
from typing import Any
|
9
9
|
|
10
|
-
from pyplumio.devices import
|
11
|
-
from pyplumio.helpers.
|
12
|
-
from pyplumio.
|
13
|
-
|
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
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
51
|
-
|
38
|
+
@event_listener
|
39
|
+
async def on_event_mixer_parameters(
|
40
|
+
self, parameters: list[tuple[int, ParameterValues]]
|
52
41
|
) -> bool:
|
53
|
-
"""
|
54
|
-
|
55
|
-
|
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 =
|
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
|
-
|
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"]
|