PyPlumIO 0.6.1__py3-none-any.whl → 0.6.2__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 -1
- pyplumio/_version.py +2 -2
- pyplumio/const.py +0 -5
- pyplumio/data_types.py +2 -2
- pyplumio/devices/__init__.py +23 -5
- pyplumio/devices/ecomax.py +30 -53
- pyplumio/devices/ecoster.py +2 -3
- pyplumio/filters.py +199 -136
- pyplumio/frames/__init__.py +101 -15
- pyplumio/frames/messages.py +8 -65
- pyplumio/frames/requests.py +38 -38
- pyplumio/frames/responses.py +30 -86
- pyplumio/helpers/async_cache.py +13 -8
- pyplumio/helpers/event_manager.py +24 -18
- pyplumio/helpers/factory.py +0 -3
- pyplumio/parameters/__init__.py +38 -35
- pyplumio/protocol.py +14 -8
- pyplumio/structures/alerts.py +2 -2
- pyplumio/structures/ecomax_parameters.py +1 -1
- pyplumio/structures/frame_versions.py +3 -2
- pyplumio/structures/mixer_parameters.py +5 -3
- pyplumio/structures/network_info.py +1 -0
- pyplumio/structures/product_info.py +1 -1
- pyplumio/structures/program_version.py +2 -2
- pyplumio/structures/schedules.py +8 -40
- pyplumio/structures/sensor_data.py +498 -0
- pyplumio/structures/thermostat_parameters.py +7 -4
- pyplumio/utils.py +41 -4
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/METADATA +4 -4
- pyplumio-0.6.2.dist-info/RECORD +50 -0
- pyplumio/structures/boiler_load.py +0 -32
- pyplumio/structures/boiler_power.py +0 -33
- pyplumio/structures/fan_power.py +0 -33
- pyplumio/structures/fuel_consumption.py +0 -36
- pyplumio/structures/fuel_level.py +0 -39
- pyplumio/structures/lambda_sensor.py +0 -57
- pyplumio/structures/mixer_sensors.py +0 -80
- pyplumio/structures/modules.py +0 -102
- pyplumio/structures/output_flags.py +0 -47
- pyplumio/structures/outputs.py +0 -88
- pyplumio/structures/pending_alerts.py +0 -28
- pyplumio/structures/statuses.py +0 -52
- pyplumio/structures/temperatures.py +0 -94
- pyplumio/structures/thermostat_sensors.py +0 -106
- pyplumio-0.6.1.dist-info/RECORD +0 -63
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,498 @@
|
|
1
|
+
"""Contains sensor data decoder."""
|
2
|
+
|
3
|
+
from collections.abc import Generator, MutableMapping
|
4
|
+
from contextlib import suppress
|
5
|
+
from dataclasses import dataclass
|
6
|
+
import math
|
7
|
+
import struct
|
8
|
+
from typing import Any, Final
|
9
|
+
|
10
|
+
from typing_extensions import TypeVar
|
11
|
+
|
12
|
+
from pyplumio.const import ATTR_SCHEDULE, BYTE_UNDEFINED, DeviceState, LambdaState
|
13
|
+
from pyplumio.data_types import Float, UnsignedInt, UnsignedShort
|
14
|
+
from pyplumio.structures import StructureDecoder
|
15
|
+
from pyplumio.utils import ensure_dict
|
16
|
+
|
17
|
+
ATTR_AIR_IN_TEMP: Final = "air_in_temp"
|
18
|
+
ATTR_AIR_OUT_TEMP: Final = "air_out_temp"
|
19
|
+
ATTR_ALARM: Final = "alarm"
|
20
|
+
ATTR_BLOW_FAN1: Final = "blow_fan1"
|
21
|
+
ATTR_BLOW_FAN2: Final = "blow_fan2"
|
22
|
+
ATTR_BOILER_LOAD: Final = "boiler_load"
|
23
|
+
ATTR_BOILER_POWER: Final = "boiler_power"
|
24
|
+
ATTR_CIRCULATION_PUMP_FLAG: Final = "circulation_pump_flag"
|
25
|
+
ATTR_CIRCULATION_PUMP: Final = "circulation_pump"
|
26
|
+
ATTR_CONTACTS: Final = "contacts"
|
27
|
+
ATTR_CURRENT_TEMP: Final = "current_temp"
|
28
|
+
ATTR_ECOLAMBDA: Final = "ecolambda"
|
29
|
+
ATTR_ECOSTER: Final = "ecoster"
|
30
|
+
ATTR_EXCHANGER_TEMP: Final = "exchanger_temp"
|
31
|
+
ATTR_EXHAUST_TEMP: Final = "exhaust_temp"
|
32
|
+
ATTR_FAN_POWER: Final = "fan_power"
|
33
|
+
ATTR_FAN: Final = "fan"
|
34
|
+
ATTR_FAN2_EXHAUST: Final = "fan2_exhaust"
|
35
|
+
ATTR_FEEDER_TEMP: Final = "feeder_temp"
|
36
|
+
ATTR_FEEDER: Final = "feeder"
|
37
|
+
ATTR_FEEDER2: Final = "feeder2"
|
38
|
+
ATTR_FIREPLACE_PUMP: Final = "fireplace_pump"
|
39
|
+
ATTR_FIREPLACE_TEMP: Final = "fireplace_temp"
|
40
|
+
ATTR_FUEL_CONSUMPTION: Final = "fuel_consumption"
|
41
|
+
ATTR_FUEL_LEVEL: Final = "fuel_level"
|
42
|
+
ATTR_GCZ_CONTACT: Final = "gcz_contact"
|
43
|
+
ATTR_HEATING_PUMP_FLAG: Final = "heating_pump_flag"
|
44
|
+
ATTR_HEATING_PUMP: Final = "heating_pump"
|
45
|
+
ATTR_HEATING_STATUS: Final = "heating_status"
|
46
|
+
ATTR_HEATING_TARGET: Final = "heating_target"
|
47
|
+
ATTR_HEATING_TEMP: Final = "heating_temp"
|
48
|
+
ATTR_HYDRAULIC_COUPLER_TEMP: Final = "hydraulic_coupler_temp"
|
49
|
+
ATTR_LAMBDA_LEVEL: Final = "lambda_level"
|
50
|
+
ATTR_LAMBDA_STATE: Final = "lambda_state"
|
51
|
+
ATTR_LAMBDA_TARGET: Final = "lambda_target"
|
52
|
+
ATTR_LIGHTER: Final = "lighter"
|
53
|
+
ATTR_LOWER_BUFFER_TEMP: Final = "lower_buffer_temp"
|
54
|
+
ATTR_LOWER_SOLAR_TEMP: Final = "lower_solar_temp"
|
55
|
+
ATTR_MIXER_SENSORS: Final = "mixer_sensors"
|
56
|
+
ATTR_MIXERS_AVAILABLE: Final = "mixers_available"
|
57
|
+
ATTR_MIXERS_CONNECTED: Final = "mixers_connected"
|
58
|
+
ATTR_MODULE_A: Final = "module_a"
|
59
|
+
ATTR_MODULE_B: Final = "module_b"
|
60
|
+
ATTR_MODULE_C: Final = "module_c"
|
61
|
+
ATTR_MODULES: Final = "modules"
|
62
|
+
ATTR_OPTICAL_TEMP: Final = "optical_temp"
|
63
|
+
ATTR_OUTER_BOILER: Final = "outer_boiler"
|
64
|
+
ATTR_OUTER_FEEDER: Final = "outer_feeder"
|
65
|
+
ATTR_OUTSIDE_TEMP: Final = "outside_temp"
|
66
|
+
ATTR_PANEL: Final = "panel"
|
67
|
+
ATTR_PENDING_ALERTS: Final = "pending_alerts"
|
68
|
+
ATTR_PUMP: Final = "pump"
|
69
|
+
ATTR_RETURN_TEMP: Final = "return_temp"
|
70
|
+
ATTR_SOLAR_PUMP_FLAG: Final = "solar_pump_flag"
|
71
|
+
ATTR_SOLAR_PUMP: Final = "solar_pump"
|
72
|
+
ATTR_STATE: Final = "state"
|
73
|
+
ATTR_TARGET_TEMP: Final = "target_temp"
|
74
|
+
ATTR_THERMOSTAT_SENSORS: Final = "thermostat_sensors"
|
75
|
+
ATTR_THERMOSTAT: Final = "thermostat"
|
76
|
+
ATTR_THERMOSTATS_AVAILABLE: Final = "thermostats_available"
|
77
|
+
ATTR_THERMOSTATS_CONNECTED: Final = "thermostats_connected"
|
78
|
+
ATTR_TOTAL_GAIN: Final = "total_gain"
|
79
|
+
ATTR_TRANSMISSION: Final = "transmission"
|
80
|
+
ATTR_UPPER_BUFFER_TEMP: Final = "upper_buffer_temp"
|
81
|
+
ATTR_UPPER_SOLAR_TEMP: Final = "upper_solar_temp"
|
82
|
+
ATTR_WATER_HEATER_PUMP_FLAG: Final = "water_heater_pump_flag"
|
83
|
+
ATTR_WATER_HEATER_PUMP: Final = "water_heater_pump"
|
84
|
+
ATTR_WATER_HEATER_STATUS: Final = "water_heater_status"
|
85
|
+
ATTR_WATER_HEATER_TARGET: Final = "water_heater_target"
|
86
|
+
ATTR_WATER_HEATER_TEMP: Final = "water_heater_temp"
|
87
|
+
|
88
|
+
OUTPUTS: tuple[str, ...] = (
|
89
|
+
ATTR_FAN,
|
90
|
+
ATTR_FEEDER,
|
91
|
+
ATTR_HEATING_PUMP,
|
92
|
+
ATTR_WATER_HEATER_PUMP,
|
93
|
+
ATTR_CIRCULATION_PUMP,
|
94
|
+
ATTR_LIGHTER,
|
95
|
+
ATTR_ALARM,
|
96
|
+
ATTR_OUTER_BOILER,
|
97
|
+
ATTR_FAN2_EXHAUST,
|
98
|
+
ATTR_FEEDER2,
|
99
|
+
ATTR_OUTER_FEEDER,
|
100
|
+
ATTR_SOLAR_PUMP,
|
101
|
+
ATTR_FIREPLACE_PUMP,
|
102
|
+
ATTR_GCZ_CONTACT,
|
103
|
+
ATTR_BLOW_FAN1,
|
104
|
+
ATTR_BLOW_FAN2,
|
105
|
+
)
|
106
|
+
|
107
|
+
TEMPERATURES: tuple[str, ...] = (
|
108
|
+
ATTR_HEATING_TEMP,
|
109
|
+
ATTR_FEEDER_TEMP,
|
110
|
+
ATTR_WATER_HEATER_TEMP,
|
111
|
+
ATTR_OUTSIDE_TEMP,
|
112
|
+
ATTR_RETURN_TEMP,
|
113
|
+
ATTR_EXHAUST_TEMP,
|
114
|
+
ATTR_OPTICAL_TEMP,
|
115
|
+
ATTR_UPPER_BUFFER_TEMP,
|
116
|
+
ATTR_LOWER_BUFFER_TEMP,
|
117
|
+
ATTR_UPPER_SOLAR_TEMP,
|
118
|
+
ATTR_LOWER_SOLAR_TEMP,
|
119
|
+
ATTR_FIREPLACE_TEMP,
|
120
|
+
ATTR_TOTAL_GAIN,
|
121
|
+
ATTR_HYDRAULIC_COUPLER_TEMP,
|
122
|
+
ATTR_EXCHANGER_TEMP,
|
123
|
+
ATTR_AIR_IN_TEMP,
|
124
|
+
ATTR_AIR_OUT_TEMP,
|
125
|
+
)
|
126
|
+
|
127
|
+
STATUSES: tuple[str, ...] = (
|
128
|
+
ATTR_HEATING_TARGET,
|
129
|
+
ATTR_HEATING_STATUS,
|
130
|
+
ATTR_WATER_HEATER_TARGET,
|
131
|
+
ATTR_WATER_HEATER_STATUS,
|
132
|
+
)
|
133
|
+
|
134
|
+
MODULES: tuple[str, ...] = (
|
135
|
+
ATTR_MODULE_A,
|
136
|
+
ATTR_MODULE_B,
|
137
|
+
ATTR_MODULE_C,
|
138
|
+
ATTR_ECOLAMBDA,
|
139
|
+
ATTR_ECOSTER,
|
140
|
+
ATTR_PANEL,
|
141
|
+
)
|
142
|
+
|
143
|
+
FUEL_LEVEL_OFFSET: Final = 101
|
144
|
+
|
145
|
+
|
146
|
+
@dataclass(slots=True, kw_only=True)
|
147
|
+
class ConnectedModules:
|
148
|
+
"""Represents a firmware version info for connected module."""
|
149
|
+
|
150
|
+
module_a: str | None = None
|
151
|
+
module_b: str | None = None
|
152
|
+
module_c: str | None = None
|
153
|
+
ecolambda: str | None = None
|
154
|
+
ecoster: str | None = None
|
155
|
+
panel: str | None = None
|
156
|
+
|
157
|
+
|
158
|
+
struct_version = struct.Struct("<BBB")
|
159
|
+
struct_vendor = struct.Struct("<BB")
|
160
|
+
|
161
|
+
_DataT = TypeVar("_DataT", bound=MutableMapping)
|
162
|
+
|
163
|
+
|
164
|
+
class SensorDataStructure(StructureDecoder):
|
165
|
+
"""Represents a sensor data structure."""
|
166
|
+
|
167
|
+
__slots__ = ("_offset",)
|
168
|
+
|
169
|
+
_offset: int
|
170
|
+
|
171
|
+
def _decode_outputs(self, message: bytearray, data: _DataT) -> _DataT:
|
172
|
+
"""Decode outputs from message."""
|
173
|
+
outputs = UnsignedInt.from_bytes(message, self._offset)
|
174
|
+
self._offset += outputs.size
|
175
|
+
for index, output in enumerate(OUTPUTS):
|
176
|
+
data[output] = bool(outputs.value & 2**index)
|
177
|
+
|
178
|
+
return data
|
179
|
+
|
180
|
+
def _decode_output_flags(self, message: bytearray, data: _DataT) -> _DataT:
|
181
|
+
"""Decode output flags from message."""
|
182
|
+
output_flags = UnsignedInt.from_bytes(message, self._offset)
|
183
|
+
self._offset += output_flags.size
|
184
|
+
data[ATTR_HEATING_PUMP_FLAG] = bool(output_flags.value & 0x04)
|
185
|
+
data[ATTR_WATER_HEATER_PUMP_FLAG] = bool(output_flags.value & 0x08)
|
186
|
+
data[ATTR_CIRCULATION_PUMP_FLAG] = bool(output_flags.value & 0x10)
|
187
|
+
data[ATTR_SOLAR_PUMP_FLAG] = bool(output_flags.value & 0x800)
|
188
|
+
return data
|
189
|
+
|
190
|
+
def _decode_temperatures(self, message: bytearray, data: _DataT) -> _DataT:
|
191
|
+
"""Decode temperatures from message."""
|
192
|
+
offset = self._offset
|
193
|
+
temperatures = message[offset]
|
194
|
+
offset += 1
|
195
|
+
for _ in range(temperatures):
|
196
|
+
index = message[offset]
|
197
|
+
offset += 1
|
198
|
+
temp = Float.from_bytes(message, offset)
|
199
|
+
offset += temp.size
|
200
|
+
if (not math.isnan(temp.value)) and 0 <= index < len(TEMPERATURES):
|
201
|
+
# Temperature exists and index is in the correct range.
|
202
|
+
data[TEMPERATURES[index]] = temp.value
|
203
|
+
|
204
|
+
self._offset = offset
|
205
|
+
return data
|
206
|
+
|
207
|
+
def _decode_statuses(self, message: bytearray, data: _DataT) -> _DataT:
|
208
|
+
"""Decode statuses from message."""
|
209
|
+
for index, status in enumerate(STATUSES):
|
210
|
+
data[status] = message[self._offset + index]
|
211
|
+
|
212
|
+
self._offset += len(STATUSES)
|
213
|
+
return data
|
214
|
+
|
215
|
+
def _decode_pending_alerts(self, message: bytearray, data: _DataT) -> _DataT:
|
216
|
+
"""Decode pending alerts from message."""
|
217
|
+
pending_alerts = message[self._offset]
|
218
|
+
data[ATTR_PENDING_ALERTS] = pending_alerts
|
219
|
+
self._offset += pending_alerts + 1
|
220
|
+
return data
|
221
|
+
|
222
|
+
def _decode_fuel_level(self, message: bytearray, data: _DataT) -> _DataT:
|
223
|
+
"""Decode fuel level from message."""
|
224
|
+
fuel_level = message[self._offset]
|
225
|
+
self._offset += 1
|
226
|
+
if fuel_level != BYTE_UNDEFINED:
|
227
|
+
# Fuel offset requirement on at least ecoMAX 860P6-O.
|
228
|
+
# See: https://github.com/denpamusic/PyPlumIO/issues/19
|
229
|
+
data[ATTR_FUEL_LEVEL] = (
|
230
|
+
fuel_level
|
231
|
+
if fuel_level < FUEL_LEVEL_OFFSET
|
232
|
+
else fuel_level - FUEL_LEVEL_OFFSET
|
233
|
+
)
|
234
|
+
|
235
|
+
return data
|
236
|
+
|
237
|
+
def _decode_boiler_load(self, message: bytearray, data: _DataT) -> _DataT:
|
238
|
+
"""Decode boiler load from message."""
|
239
|
+
boiler_load = message[self._offset]
|
240
|
+
self._offset += 1
|
241
|
+
if boiler_load != BYTE_UNDEFINED:
|
242
|
+
data[ATTR_BOILER_LOAD] = boiler_load
|
243
|
+
|
244
|
+
return data
|
245
|
+
|
246
|
+
def _decode_float_value(
|
247
|
+
self, name: str, message: bytearray, data: _DataT
|
248
|
+
) -> _DataT:
|
249
|
+
"""Decode float value and increase an offset."""
|
250
|
+
float_value = Float.from_bytes(message, self._offset)
|
251
|
+
self._offset += float_value.size
|
252
|
+
if not math.isnan(float_value.value):
|
253
|
+
data[name] = float_value.value
|
254
|
+
|
255
|
+
return data
|
256
|
+
|
257
|
+
def _decode_modules(self, message: bytearray, data: _DataT) -> _DataT:
|
258
|
+
"""Decode modules from message."""
|
259
|
+
offset = self._offset
|
260
|
+
|
261
|
+
def _module_versions() -> Generator[tuple[str, str | None]]:
|
262
|
+
"""Unpack a module version."""
|
263
|
+
nonlocal offset
|
264
|
+
for module in MODULES:
|
265
|
+
if message[offset] != BYTE_UNDEFINED:
|
266
|
+
version_data = struct_version.unpack_from(message, offset)
|
267
|
+
version = ".".join(str(i) for i in version_data)
|
268
|
+
offset += struct_version.size
|
269
|
+
if module == ATTR_MODULE_A:
|
270
|
+
vendor_code, vendor_version = struct_vendor.unpack_from(
|
271
|
+
message, offset
|
272
|
+
)
|
273
|
+
version += f".{chr(vendor_code) + str(vendor_version)}"
|
274
|
+
offset += struct_vendor.size
|
275
|
+
else:
|
276
|
+
offset += 1
|
277
|
+
version = None
|
278
|
+
|
279
|
+
yield module, version
|
280
|
+
|
281
|
+
data[ATTR_MODULES] = ConnectedModules(**dict(_module_versions()))
|
282
|
+
self._offset = offset
|
283
|
+
return data
|
284
|
+
|
285
|
+
def _decode_lambda_sensor(self, message: bytearray, data: _DataT) -> _DataT:
|
286
|
+
"""Decode lambda sensor from message."""
|
287
|
+
offset = self._offset
|
288
|
+
lambda_state = message[offset]
|
289
|
+
offset += 1
|
290
|
+
if lambda_state != BYTE_UNDEFINED:
|
291
|
+
lambda_target = message[offset]
|
292
|
+
offset += 1
|
293
|
+
level = UnsignedShort.from_bytes(message, offset)
|
294
|
+
offset += level.size
|
295
|
+
with suppress(ValueError):
|
296
|
+
lambda_state = LambdaState(lambda_state)
|
297
|
+
|
298
|
+
data[ATTR_LAMBDA_STATE] = lambda_state
|
299
|
+
data[ATTR_LAMBDA_TARGET] = lambda_target
|
300
|
+
data[ATTR_LAMBDA_LEVEL] = level.value / 10
|
301
|
+
|
302
|
+
self._offset = offset
|
303
|
+
return data
|
304
|
+
|
305
|
+
def _decode_thermostat_sensors(self, message: bytearray, data: _DataT) -> _DataT:
|
306
|
+
"""Decode thermostat sensors from message."""
|
307
|
+
contact_mask = 1
|
308
|
+
schedule_mask = 1 << 3
|
309
|
+
offset = self._offset
|
310
|
+
|
311
|
+
def _unpack_thermostat_sensors(contacts: int) -> dict[str, Any] | None:
|
312
|
+
"""Unpack sensors for a single thermostat."""
|
313
|
+
nonlocal offset, contact_mask, schedule_mask
|
314
|
+
state = message[offset]
|
315
|
+
offset += 1
|
316
|
+
current_temp = Float.from_bytes(message, offset)
|
317
|
+
offset += current_temp.size
|
318
|
+
target_temp = Float.from_bytes(message, offset)
|
319
|
+
offset += target_temp.size
|
320
|
+
contacts_state = bool(contacts & contact_mask)
|
321
|
+
contact_mask <<= 1
|
322
|
+
schedule_state = bool(contacts & schedule_mask)
|
323
|
+
schedule_mask <<= 1
|
324
|
+
|
325
|
+
if math.isnan(current_temp.value) or target_temp.value <= 0:
|
326
|
+
return None
|
327
|
+
|
328
|
+
return {
|
329
|
+
ATTR_STATE: state,
|
330
|
+
ATTR_CURRENT_TEMP: current_temp.value,
|
331
|
+
ATTR_TARGET_TEMP: target_temp.value,
|
332
|
+
ATTR_CONTACTS: contacts_state,
|
333
|
+
ATTR_SCHEDULE: schedule_state,
|
334
|
+
}
|
335
|
+
|
336
|
+
def _thermostat_sensors(contacts: int) -> Generator[tuple[int, dict[str, Any]]]:
|
337
|
+
"""Get thermostat sensors."""
|
338
|
+
for index in range(thermostats):
|
339
|
+
if sensors := _unpack_thermostat_sensors(contacts):
|
340
|
+
yield (index, sensors)
|
341
|
+
|
342
|
+
contacts = message[offset]
|
343
|
+
offset += 1
|
344
|
+
if contacts != BYTE_UNDEFINED:
|
345
|
+
thermostats = message[offset]
|
346
|
+
offset += 1
|
347
|
+
thermostat_sensors = dict(_thermostat_sensors(contacts))
|
348
|
+
data[ATTR_THERMOSTAT_SENSORS] = thermostat_sensors
|
349
|
+
data[ATTR_THERMOSTATS_CONNECTED] = len(thermostat_sensors)
|
350
|
+
data[ATTR_THERMOSTATS_AVAILABLE] = thermostats
|
351
|
+
|
352
|
+
self._offset = offset
|
353
|
+
return data
|
354
|
+
|
355
|
+
def _decode_mixer_sensors(self, message: bytearray, data: _DataT) -> _DataT:
|
356
|
+
"""Decode mixer sensors from message."""
|
357
|
+
offset = self._offset
|
358
|
+
|
359
|
+
def _unpack_mixer_sensors() -> dict[str, Any] | None:
|
360
|
+
"""Unpack sensors for a single mixer."""
|
361
|
+
nonlocal offset
|
362
|
+
current_temp = Float.from_bytes(message, offset)
|
363
|
+
offset += current_temp.size
|
364
|
+
data = None
|
365
|
+
if not math.isnan(current_temp.value):
|
366
|
+
data = {
|
367
|
+
ATTR_CURRENT_TEMP: current_temp.value,
|
368
|
+
ATTR_TARGET_TEMP: message[offset],
|
369
|
+
ATTR_PUMP: bool(message[offset + 2] & 0x01),
|
370
|
+
}
|
371
|
+
|
372
|
+
offset += 4
|
373
|
+
return data
|
374
|
+
|
375
|
+
def _mixer_sensors(mixers: int) -> Generator[tuple[int, dict[str, Any]]]:
|
376
|
+
"""Get mixer sensors."""
|
377
|
+
for index in range(mixers):
|
378
|
+
if sensors := _unpack_mixer_sensors():
|
379
|
+
yield (index, sensors)
|
380
|
+
|
381
|
+
mixers = message[offset]
|
382
|
+
offset += 1
|
383
|
+
mixer_sensors = dict(_mixer_sensors(mixers))
|
384
|
+
data[ATTR_MIXER_SENSORS] = mixer_sensors
|
385
|
+
data[ATTR_MIXERS_CONNECTED] = len(mixer_sensors)
|
386
|
+
data[ATTR_MIXERS_AVAILABLE] = mixers
|
387
|
+
self._offset = offset
|
388
|
+
return data
|
389
|
+
|
390
|
+
def decode(
|
391
|
+
self, message: bytearray, offset: int = 0, data: dict[str, Any] | None = None
|
392
|
+
) -> tuple[dict[str, Any], int]:
|
393
|
+
"""Decode bytes and return message data and offset."""
|
394
|
+
data = ensure_dict(data)
|
395
|
+
data[ATTR_STATE] = message[offset]
|
396
|
+
self._offset = offset + 1
|
397
|
+
with suppress(ValueError):
|
398
|
+
data[ATTR_STATE] = DeviceState(data[ATTR_STATE])
|
399
|
+
|
400
|
+
data = self._decode_outputs(message, data)
|
401
|
+
data = self._decode_output_flags(message, data)
|
402
|
+
data = self._decode_temperatures(message, data)
|
403
|
+
data = self._decode_statuses(message, data)
|
404
|
+
data = self._decode_pending_alerts(message, data)
|
405
|
+
data = self._decode_fuel_level(message, data)
|
406
|
+
data[ATTR_TRANSMISSION] = message[self._offset]
|
407
|
+
self._offset += 1
|
408
|
+
data = self._decode_float_value(ATTR_FAN_POWER, message, data)
|
409
|
+
data = self._decode_boiler_load(message, data)
|
410
|
+
data = self._decode_float_value(ATTR_BOILER_POWER, message, data)
|
411
|
+
data = self._decode_float_value(ATTR_FUEL_CONSUMPTION, message, data)
|
412
|
+
data[ATTR_THERMOSTAT] = message[self._offset]
|
413
|
+
self._offset += 1
|
414
|
+
data = self._decode_modules(message, data)
|
415
|
+
data = self._decode_lambda_sensor(message, data)
|
416
|
+
data = self._decode_thermostat_sensors(message, data)
|
417
|
+
data = self._decode_mixer_sensors(message, data)
|
418
|
+
return data, offset
|
419
|
+
|
420
|
+
|
421
|
+
__all__ = [
|
422
|
+
"ATTR_AIR_IN_TEMP",
|
423
|
+
"ATTR_AIR_OUT_TEMP",
|
424
|
+
"ATTR_ALARM",
|
425
|
+
"ATTR_BLOW_FAN1",
|
426
|
+
"ATTR_BLOW_FAN2",
|
427
|
+
"ATTR_BOILER_LOAD",
|
428
|
+
"ATTR_BOILER_POWER",
|
429
|
+
"ATTR_CIRCULATION_PUMP",
|
430
|
+
"ATTR_CIRCULATION_PUMP_FLAG",
|
431
|
+
"ATTR_CONTACTS",
|
432
|
+
"ATTR_CURRENT_TEMP",
|
433
|
+
"ATTR_ECOLAMBDA",
|
434
|
+
"ATTR_ECOSTER",
|
435
|
+
"ATTR_EXCHANGER_TEMP",
|
436
|
+
"ATTR_EXHAUST_TEMP",
|
437
|
+
"ATTR_FAN",
|
438
|
+
"ATTR_FAN2_EXHAUST",
|
439
|
+
"ATTR_FAN_POWER",
|
440
|
+
"ATTR_FEEDER",
|
441
|
+
"ATTR_FEEDER2",
|
442
|
+
"ATTR_FEEDER_TEMP",
|
443
|
+
"ATTR_FIREPLACE_PUMP",
|
444
|
+
"ATTR_FIREPLACE_TEMP",
|
445
|
+
"ATTR_FUEL_CONSUMPTION",
|
446
|
+
"ATTR_FUEL_LEVEL",
|
447
|
+
"ATTR_GCZ_CONTACT",
|
448
|
+
"ATTR_HEATING_PUMP",
|
449
|
+
"ATTR_HEATING_PUMP_FLAG",
|
450
|
+
"ATTR_HEATING_STATUS",
|
451
|
+
"ATTR_HEATING_TARGET",
|
452
|
+
"ATTR_HEATING_TEMP",
|
453
|
+
"ATTR_HYDRAULIC_COUPLER_TEMP",
|
454
|
+
"ATTR_LAMBDA_LEVEL",
|
455
|
+
"ATTR_LAMBDA_STATE",
|
456
|
+
"ATTR_LAMBDA_TARGET",
|
457
|
+
"ATTR_LIGHTER",
|
458
|
+
"ATTR_LOWER_BUFFER_TEMP",
|
459
|
+
"ATTR_LOWER_SOLAR_TEMP",
|
460
|
+
"ATTR_MIXER_SENSORS",
|
461
|
+
"ATTR_MIXERS_AVAILABLE",
|
462
|
+
"ATTR_MIXERS_CONNECTED",
|
463
|
+
"ATTR_MODULE_A",
|
464
|
+
"ATTR_MODULE_B",
|
465
|
+
"ATTR_MODULE_C",
|
466
|
+
"ATTR_MODULES",
|
467
|
+
"ATTR_OPTICAL_TEMP",
|
468
|
+
"ATTR_OUTER_BOILER",
|
469
|
+
"ATTR_OUTER_FEEDER",
|
470
|
+
"ATTR_OUTSIDE_TEMP",
|
471
|
+
"ATTR_PANEL",
|
472
|
+
"ATTR_PENDING_ALERTS",
|
473
|
+
"ATTR_PUMP",
|
474
|
+
"ATTR_RETURN_TEMP",
|
475
|
+
"ATTR_SOLAR_PUMP",
|
476
|
+
"ATTR_SOLAR_PUMP_FLAG",
|
477
|
+
"ATTR_STATE",
|
478
|
+
"ATTR_TARGET_TEMP",
|
479
|
+
"ATTR_THERMOSTAT",
|
480
|
+
"ATTR_THERMOSTAT_SENSORS",
|
481
|
+
"ATTR_THERMOSTATS_AVAILABLE",
|
482
|
+
"ATTR_THERMOSTATS_CONNECTED",
|
483
|
+
"ATTR_TOTAL_GAIN",
|
484
|
+
"ATTR_TRANSMISSION",
|
485
|
+
"ATTR_UPPER_BUFFER_TEMP",
|
486
|
+
"ATTR_UPPER_SOLAR_TEMP",
|
487
|
+
"ATTR_WATER_HEATER_PUMP",
|
488
|
+
"ATTR_WATER_HEATER_PUMP_FLAG",
|
489
|
+
"ATTR_WATER_HEATER_STATUS",
|
490
|
+
"ATTR_WATER_HEATER_TARGET",
|
491
|
+
"ATTR_WATER_HEATER_TEMP",
|
492
|
+
"ConnectedModules",
|
493
|
+
"MODULES",
|
494
|
+
"OUTPUTS",
|
495
|
+
"SensorDataStructure",
|
496
|
+
"STATUSES",
|
497
|
+
"TEMPERATURES",
|
498
|
+
]
|
@@ -3,12 +3,12 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from collections.abc import Generator
|
6
|
-
from typing import Any, Final
|
6
|
+
from typing import Any, Final, TypeAlias
|
7
7
|
|
8
8
|
from pyplumio.parameters import ParameterValues, unpack_parameter
|
9
9
|
from pyplumio.parameters.thermostat import get_thermostat_parameter_types
|
10
10
|
from pyplumio.structures import StructureDecoder
|
11
|
-
from pyplumio.structures.
|
11
|
+
from pyplumio.structures.sensor_data import ATTR_THERMOSTATS_AVAILABLE
|
12
12
|
from pyplumio.utils import ensure_dict
|
13
13
|
|
14
14
|
ATTR_THERMOSTAT_PROFILE: Final = "thermostat_profile"
|
@@ -17,6 +17,9 @@ ATTR_THERMOSTAT_PARAMETERS: Final = "thermostat_parameters"
|
|
17
17
|
THERMOSTAT_PARAMETER_SIZE: Final = 3
|
18
18
|
|
19
19
|
|
20
|
+
_ParameterValues: TypeAlias = tuple[int, ParameterValues]
|
21
|
+
|
22
|
+
|
20
23
|
class ThermostatParametersStructure(StructureDecoder):
|
21
24
|
"""Represents a thermostat parameters data structure."""
|
22
25
|
|
@@ -26,7 +29,7 @@ class ThermostatParametersStructure(StructureDecoder):
|
|
26
29
|
|
27
30
|
def _thermostat_parameter(
|
28
31
|
self, message: bytearray, thermostats: int, start: int, end: int
|
29
|
-
) -> Generator[
|
32
|
+
) -> Generator[_ParameterValues]:
|
30
33
|
"""Get a single thermostat parameter."""
|
31
34
|
parameter_types = get_thermostat_parameter_types()
|
32
35
|
for index in range(start, (start + end) // thermostats):
|
@@ -40,7 +43,7 @@ class ThermostatParametersStructure(StructureDecoder):
|
|
40
43
|
|
41
44
|
def _thermostat_parameters(
|
42
45
|
self, message: bytearray, thermostats: int, start: int, end: int
|
43
|
-
) -> Generator[tuple[int, list[
|
46
|
+
) -> Generator[tuple[int, list[_ParameterValues]]]:
|
44
47
|
"""Get parameters for a thermostat."""
|
45
48
|
for index in range(thermostats):
|
46
49
|
if parameters := list(
|
pyplumio/utils.py
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Awaitable, Callable, Mapping
|
7
|
-
from functools import wraps
|
8
|
-
from typing import ParamSpec, TypeVar
|
6
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
7
|
+
from functools import reduce, wraps
|
8
|
+
from typing import Annotated, Final, ParamSpec, TypeAlias, TypeVar
|
9
9
|
|
10
10
|
KT = TypeVar("KT") # Key type.
|
11
11
|
VT = TypeVar("VT") # Value type.
|
@@ -42,6 +42,36 @@ def is_divisible(a: float, b: float, precision: int = 6) -> bool:
|
|
42
42
|
return a_scaled % b_scaled == 0
|
43
43
|
|
44
44
|
|
45
|
+
SingleByte: TypeAlias = Annotated[int, "Single byte integer between 0 and 255"]
|
46
|
+
|
47
|
+
BITS_PER_BYTE: Final = 8
|
48
|
+
|
49
|
+
|
50
|
+
def join_bits(bits: Sequence[bool]) -> SingleByte:
|
51
|
+
"""Join eight bits into a single byte."""
|
52
|
+
if len(bits) > BITS_PER_BYTE:
|
53
|
+
raise ValueError("The number of bits must not exceed 8.")
|
54
|
+
|
55
|
+
return reduce(lambda byte, bit: (byte << 1) | int(bit), bits, 0)
|
56
|
+
|
57
|
+
|
58
|
+
MAX_BYTE: Final = 255
|
59
|
+
|
60
|
+
|
61
|
+
def split_byte(byte: SingleByte) -> list[bool]:
|
62
|
+
"""Split single byte into an eight bits."""
|
63
|
+
if byte < 0 or byte > MAX_BYTE:
|
64
|
+
raise ValueError("Byte value must be between 0 and 255.")
|
65
|
+
|
66
|
+
if byte == 0:
|
67
|
+
return [False, False, False, False, False, False, False, False]
|
68
|
+
|
69
|
+
if byte == MAX_BYTE:
|
70
|
+
return [True, True, True, True, True, True, True, True]
|
71
|
+
|
72
|
+
return [bool(byte & (1 << bit)) for bit in reversed(range(8))]
|
73
|
+
|
74
|
+
|
45
75
|
T = TypeVar("T")
|
46
76
|
P = ParamSpec("P")
|
47
77
|
|
@@ -62,4 +92,11 @@ def timeout(
|
|
62
92
|
return decorator
|
63
93
|
|
64
94
|
|
65
|
-
__all__ = [
|
95
|
+
__all__ = [
|
96
|
+
"ensure_dict",
|
97
|
+
"is_divisible",
|
98
|
+
"to_camelcase",
|
99
|
+
"join_bits",
|
100
|
+
"split_byte",
|
101
|
+
"timeout",
|
102
|
+
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.2
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -32,9 +32,9 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
|
32
32
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
33
33
|
Requires-Dist: pytest==8.4.2; extra == "test"
|
34
34
|
Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
|
35
|
-
Requires-Dist: ruff==0.13.
|
36
|
-
Requires-Dist: tox==4.30.
|
37
|
-
Requires-Dist: types-pyserial==3.5.0.
|
35
|
+
Requires-Dist: ruff==0.13.3; extra == "test"
|
36
|
+
Requires-Dist: tox==4.30.3; extra == "test"
|
37
|
+
Requires-Dist: types-pyserial==3.5.0.20251001; extra == "test"
|
38
38
|
Provides-Extra: docs
|
39
39
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
40
40
|
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
@@ -0,0 +1,50 @@
|
|
1
|
+
pyplumio/__init__.py,sha256=q4TBKjxccWvsms9BHFEnd684rc32r59o4xropBaEM90,3374
|
2
|
+
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
+
pyplumio/_version.py,sha256=RY8LE6fDgFr4EFmdDWXFpTFEh_gnhjqz0rKUDqEfQ3A,704
|
4
|
+
pyplumio/connection.py,sha256=4JoutupQSvAO8WXFFuwddpJJODzna5oq-cHJRI4kgZ8,6625
|
5
|
+
pyplumio/const.py,sha256=7UMQrjmgqfpInumt67otDsGVfJNW1YRgoW7kDJT-Gy0,5439
|
6
|
+
pyplumio/data_types.py,sha256=eVhmXXzEEmze5xMY3SR307FZRDd6V6Vhba1-qRDLVFs,9430
|
7
|
+
pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
|
8
|
+
pyplumio/filters.py,sha256=a4Mfi3f2l0CT_gv95CTc796s1rpydLMMMovJN_Hov_w,17165
|
9
|
+
pyplumio/protocol.py,sha256=Z7MZLkBUX_MlONw0UBF_12ggPm3_iZ3AWxn2k7seTEo,12204
|
10
|
+
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
pyplumio/stream.py,sha256=zFMKZ_GxsSGcaBTJigVM1CK3uGjlEJXgcvKqus8MDzk,7740
|
12
|
+
pyplumio/utils.py,sha256=PIjIRPaKzxtRroDFbkwAdksL5OHt2qqyFpTDKX6yNSo,2790
|
13
|
+
pyplumio/devices/__init__.py,sha256=ItX3JBzUFkSveHtNg1P9Kn7u60yVBo02DOS0D9UPuxc,8818
|
14
|
+
pyplumio/devices/ecomax.py,sha256=oJSiG_vzwQJI7NqQr9VIoyWfxwYcvibiDrSyl3LgdMI,15840
|
15
|
+
pyplumio/devices/ecoster.py,sha256=bosGErLISuYuEGG6J1TSeEt4NexySJHexYxxSpEddco,325
|
16
|
+
pyplumio/devices/mixer.py,sha256=7WdUVgwO4VXmaPNzh3ZWpKr2ooRXWemz2KFHAw35_Rk,2731
|
17
|
+
pyplumio/devices/thermostat.py,sha256=MHMKe45fQ7jKlhBVObJ7McbYQKuF6-LOKSHy-9VNsCU,2253
|
18
|
+
pyplumio/frames/__init__.py,sha256=6BWAh7wfq2A5ZnLDe2cfGK8K_RKN9uZFl7XSXRXSFvs,10438
|
19
|
+
pyplumio/frames/messages.py,sha256=i11MpcIiICBwgJch1yUeDTJ402Z1snh1gZOiSIEaOuA,892
|
20
|
+
pyplumio/frames/requests.py,sha256=E2OcKwmQebXazlKpizv0dKX0li9gld2xPzisJAzC8z4,8026
|
21
|
+
pyplumio/frames/responses.py,sha256=xLFBEWLDY6yDs_VSlFGfqbtoCzziPNyd6Ss374ByejU,5074
|
22
|
+
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
23
|
+
pyplumio/helpers/async_cache.py,sha256=4GqMx6XhuQ84656ky6zPI8IZZGHYNSeDJextLgKCJM4,1432
|
24
|
+
pyplumio/helpers/event_manager.py,sha256=tzV9yWWYLLaF0NRyinZDyYJaumqRM7pIlOOTMpKruZw,8320
|
25
|
+
pyplumio/helpers/factory.py,sha256=OfYuWNjBiEdL9X7PqmPVvjTq1BQcu231UmE9E_nahPg,995
|
26
|
+
pyplumio/helpers/task_manager.py,sha256=N71F6Ag1HHxdf5zJeCMcEziFdH9lmJKtMPoRGjJ-E40,1209
|
27
|
+
pyplumio/parameters/__init__.py,sha256=Ye8nCoUtltpbVHx0yv0DwNHcqeVrqN0CzGyoZdjZvUA,16171
|
28
|
+
pyplumio/parameters/ecomax.py,sha256=KjHlkVZK2XYEl4HNSdCRLAnv0KEn7gjnEO_CsKFZwIw,26199
|
29
|
+
pyplumio/parameters/mixer.py,sha256=cjwe6AJdboAIEnCeiYNqIRmOVo3dSQqbMTWgiCSx8J8,6606
|
30
|
+
pyplumio/parameters/thermostat.py,sha256=sRAndI87jANM8uvdQc1LdkT6_baDxf0AEAFVYRstzNE,5039
|
31
|
+
pyplumio/parameters/custom/__init__.py,sha256=EeddoseRsh2Gxche3e3woRBgNszraOnLUs9TciK7dCA,3168
|
32
|
+
pyplumio/parameters/custom/ecomax_860d3_hb.py,sha256=IsNgDXmV90QpBilDV4fGSBtIUEQJJbR9rjnfCr3-pHE,2840
|
33
|
+
pyplumio/structures/__init__.py,sha256=tb62y-x466WSogdjNpsvqcD3Kiz7xMW604m2-yJH3jc,1329
|
34
|
+
pyplumio/structures/alerts.py,sha256=EUA-iB0ZXhaGHHY2lO_d8Hbwech2rJCPGJVjSXTUPW0,3688
|
35
|
+
pyplumio/structures/ecomax_parameters.py,sha256=FI53j-UO0qdUnquJvMBZiuCHFBUAIUsMZRGoubnTXyM,1651
|
36
|
+
pyplumio/structures/frame_versions.py,sha256=bRBHQGv19ko_9meB3YGA11KiT_UgAi8L9WDgnqMhiVk,1620
|
37
|
+
pyplumio/structures/mixer_parameters.py,sha256=qaEMpFLDB0QkMxNq5xc0aG_TkUfIg9WAGM4FbJOuocE,2065
|
38
|
+
pyplumio/structures/network_info.py,sha256=ENZbQUQgxdrsq5zrfMT7co4TNap21XJ1LqgTLS3f9Ec,4425
|
39
|
+
pyplumio/structures/product_info.py,sha256=LCf4dp-kO-9S3wk7Kz53ek-283Hxr-b0ylSkgjmsRYs,3269
|
40
|
+
pyplumio/structures/program_version.py,sha256=o0tjJhi4dqTm5M-9-UpdXFltMZeiPOxYgdkYHyXxX1Y,2572
|
41
|
+
pyplumio/structures/regulator_data.py,sha256=SYKI1YPC3mDAth-SpYejttbD0IzBfobjgC-uy_uUKnw,2333
|
42
|
+
pyplumio/structures/regulator_data_schema.py,sha256=0SapbZCGzqAHmHC7dwhufszJ9FNo_ZO_XMrFGNiUe-w,1547
|
43
|
+
pyplumio/structures/schedules.py,sha256=7VltDMpGrNdylq1iFt0tmj0mxkUYX0vsPx0ADwc5UDU,10714
|
44
|
+
pyplumio/structures/sensor_data.py,sha256=tYNqvHi28eVdgESjQ-ZkDU4mLdeZOSxOW9nw2-3FnBU,17174
|
45
|
+
pyplumio/structures/thermostat_parameters.py,sha256=XMlnpmnw06Dkqynv1e9qG2IDelfmmTYRT7UJP01RWFo,3025
|
46
|
+
pyplumio-0.6.2.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
47
|
+
pyplumio-0.6.2.dist-info/METADATA,sha256=BOtqquZbHJPAO6klTWNxYAVYbjsmIGHCT07ni6Uwxgk,5520
|
48
|
+
pyplumio-0.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
49
|
+
pyplumio-0.6.2.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
50
|
+
pyplumio-0.6.2.dist-info/RECORD,,
|