PyPlumIO 0.5.42__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.
- pyplumio/__init__.py +3 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +14 -14
- pyplumio/const.py +7 -0
- pyplumio/devices/__init__.py +32 -19
- pyplumio/devices/ecomax.py +112 -128
- pyplumio/devices/ecoster.py +5 -0
- pyplumio/devices/mixer.py +21 -31
- pyplumio/devices/thermostat.py +19 -29
- pyplumio/filters.py +166 -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/data_types.py +23 -21
- pyplumio/helpers/event_manager.py +40 -3
- pyplumio/helpers/factory.py +5 -2
- pyplumio/helpers/schedule.py +8 -5
- pyplumio/helpers/task_manager.py +3 -0
- pyplumio/helpers/timeout.py +8 -8
- pyplumio/helpers/uid.py +8 -5
- pyplumio/{helpers/parameter.py → parameters/__init__.py} +98 -4
- pyplumio/parameters/ecomax.py +868 -0
- pyplumio/parameters/mixer.py +245 -0
- pyplumio/parameters/thermostat.py +197 -0
- pyplumio/protocol.py +6 -3
- pyplumio/stream.py +3 -0
- pyplumio/structures/__init__.py +3 -0
- pyplumio/structures/alerts.py +8 -5
- pyplumio/structures/boiler_load.py +3 -0
- pyplumio/structures/boiler_power.py +3 -0
- pyplumio/structures/ecomax_parameters.py +6 -800
- pyplumio/structures/fan_power.py +3 -0
- pyplumio/structures/frame_versions.py +3 -0
- pyplumio/structures/fuel_consumption.py +3 -0
- pyplumio/structures/fuel_level.py +3 -0
- pyplumio/structures/lambda_sensor.py +8 -0
- pyplumio/structures/mixer_parameters.py +8 -230
- pyplumio/structures/mixer_sensors.py +9 -0
- pyplumio/structures/modules.py +14 -0
- pyplumio/structures/network_info.py +11 -0
- pyplumio/structures/output_flags.py +9 -0
- pyplumio/structures/outputs.py +21 -0
- pyplumio/structures/pending_alerts.py +3 -0
- pyplumio/structures/product_info.py +5 -2
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +4 -1
- pyplumio/structures/regulator_data_schema.py +3 -0
- pyplumio/structures/schedules.py +18 -1
- pyplumio/structures/statuses.py +9 -0
- pyplumio/structures/temperatures.py +22 -0
- pyplumio/structures/thermostat_parameters.py +13 -177
- pyplumio/structures/thermostat_sensors.py +9 -0
- pyplumio/utils.py +14 -12
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/METADATA +30 -17
- pyplumio-0.5.43.dist-info/RECORD +63 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/WHEEL +1 -1
- pyplumio-0.5.42.dist-info/RECORD +0 -60
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.42.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
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
|
8
|
+
from typing import Any, Final
|
9
9
|
|
10
|
-
from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE
|
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,
|
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
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
pyplumio/devices/__init__.py
CHANGED
@@ -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.
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
+
]
|
pyplumio/devices/ecomax.py
CHANGED
@@ -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.
|
27
|
+
from pyplumio.helpers.event_manager import event_listener
|
25
28
|
from pyplumio.helpers.schedule import Schedule, ScheduleDay
|
26
|
-
from pyplumio.
|
27
|
-
from pyplumio.
|
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
|
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
|
-
"""
|
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 =
|
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
|
-
|
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
|
-
|
211
|
-
|
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
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
-
|
276
|
-
|
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
|
-
"""
|
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
|
-
|
319
|
-
|
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
|
-
|
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(
|
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
|
-
|
349
|
+
@event_listener(ATTR_THERMOSTAT_PROFILE)
|
350
|
+
async def on_event_thermostat_profile(
|
366
351
|
self, values: ParameterValues | None
|
367
352
|
) -> EcomaxNumber | None:
|
368
|
-
"""
|
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
|
-
|
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
|
-
"""
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
-
|
418
|
-
|
419
|
-
|
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
|
-
|
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"]
|
pyplumio/devices/ecoster.py
CHANGED
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
|
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_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
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
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
|
-
"""
|
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 =
|
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"]
|