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
@@ -0,0 +1,245 @@
|
|
1
|
+
"""Contains mixer parameter descriptors."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from functools import cache
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from dataslots import dataslots
|
10
|
+
|
11
|
+
from pyplumio.const import (
|
12
|
+
ATTR_DEVICE_INDEX,
|
13
|
+
ATTR_INDEX,
|
14
|
+
ATTR_VALUE,
|
15
|
+
FrameType,
|
16
|
+
ProductType,
|
17
|
+
UnitOfMeasurement,
|
18
|
+
)
|
19
|
+
from pyplumio.frames import Request
|
20
|
+
from pyplumio.parameters import (
|
21
|
+
OffsetNumber,
|
22
|
+
OffsetNumberDescription,
|
23
|
+
Parameter,
|
24
|
+
ParameterDescription,
|
25
|
+
Switch,
|
26
|
+
SwitchDescription,
|
27
|
+
)
|
28
|
+
from pyplumio.structures.product_info import ProductInfo
|
29
|
+
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from pyplumio.devices.mixer import Mixer
|
32
|
+
|
33
|
+
|
34
|
+
@dataclass
|
35
|
+
class MixerParameterDescription(ParameterDescription):
|
36
|
+
"""Represents a mixer parameter description."""
|
37
|
+
|
38
|
+
__slots__ = ()
|
39
|
+
|
40
|
+
|
41
|
+
class MixerParameter(Parameter):
|
42
|
+
"""Represent a mixer parameter."""
|
43
|
+
|
44
|
+
__slots__ = ()
|
45
|
+
|
46
|
+
device: Mixer
|
47
|
+
description: MixerParameterDescription
|
48
|
+
|
49
|
+
async def create_request(self) -> Request:
|
50
|
+
"""Create a request to change the parameter."""
|
51
|
+
return await Request.create(
|
52
|
+
FrameType.REQUEST_SET_MIXER_PARAMETER,
|
53
|
+
recipient=self.device.parent.address,
|
54
|
+
data={
|
55
|
+
ATTR_INDEX: self._index,
|
56
|
+
ATTR_VALUE: self.values.value,
|
57
|
+
ATTR_DEVICE_INDEX: self.device.index,
|
58
|
+
},
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
@dataclass
|
63
|
+
class MixerNumberDescription(MixerParameterDescription, OffsetNumberDescription):
|
64
|
+
"""Represent a mixer number description."""
|
65
|
+
|
66
|
+
__slots__ = ()
|
67
|
+
|
68
|
+
|
69
|
+
class MixerNumber(MixerParameter, OffsetNumber):
|
70
|
+
"""Represents a mixer number."""
|
71
|
+
|
72
|
+
__slots__ = ()
|
73
|
+
|
74
|
+
description: MixerNumberDescription
|
75
|
+
|
76
|
+
|
77
|
+
@dataslots
|
78
|
+
@dataclass
|
79
|
+
class MixerSwitchDescription(MixerParameterDescription, SwitchDescription):
|
80
|
+
"""Represents a mixer switch description."""
|
81
|
+
|
82
|
+
|
83
|
+
class MixerSwitch(MixerParameter, Switch):
|
84
|
+
"""Represents a mixer switch."""
|
85
|
+
|
86
|
+
__slots__ = ()
|
87
|
+
|
88
|
+
description: MixerSwitchDescription
|
89
|
+
|
90
|
+
|
91
|
+
PARAMETER_TYPES: dict[ProductType, list[MixerParameterDescription]] = {
|
92
|
+
ProductType.ECOMAX_P: [
|
93
|
+
MixerNumberDescription(
|
94
|
+
name="mixer_target_temp",
|
95
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
96
|
+
),
|
97
|
+
MixerNumberDescription(
|
98
|
+
name="min_target_temp",
|
99
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
100
|
+
),
|
101
|
+
MixerNumberDescription(
|
102
|
+
name="max_target_temp",
|
103
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
104
|
+
),
|
105
|
+
MixerNumberDescription(
|
106
|
+
name="thermostat_decrease_target_temp",
|
107
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
108
|
+
),
|
109
|
+
MixerSwitchDescription(
|
110
|
+
name="weather_control",
|
111
|
+
),
|
112
|
+
MixerNumberDescription(
|
113
|
+
name="heating_curve",
|
114
|
+
step=0.1,
|
115
|
+
),
|
116
|
+
MixerNumberDescription(
|
117
|
+
name="heating_curve_shift",
|
118
|
+
offset=20,
|
119
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
120
|
+
),
|
121
|
+
MixerNumberDescription(
|
122
|
+
name="weather_factor",
|
123
|
+
),
|
124
|
+
MixerNumberDescription(
|
125
|
+
name="work_mode",
|
126
|
+
),
|
127
|
+
MixerNumberDescription(
|
128
|
+
name="mixer_input_dead_zone",
|
129
|
+
step=0.1,
|
130
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
131
|
+
),
|
132
|
+
MixerSwitchDescription(
|
133
|
+
name="thermostat_operation",
|
134
|
+
),
|
135
|
+
MixerNumberDescription(
|
136
|
+
name="thermostat_mode",
|
137
|
+
),
|
138
|
+
MixerSwitchDescription(
|
139
|
+
name="disable_pump_on_thermostat",
|
140
|
+
),
|
141
|
+
MixerSwitchDescription(
|
142
|
+
name="summer_work",
|
143
|
+
),
|
144
|
+
],
|
145
|
+
ProductType.ECOMAX_I: [
|
146
|
+
MixerNumberDescription(
|
147
|
+
name="work_mode",
|
148
|
+
),
|
149
|
+
MixerNumberDescription(
|
150
|
+
name="circuit_target_temp",
|
151
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
152
|
+
),
|
153
|
+
MixerNumberDescription(
|
154
|
+
name="day_target_temp",
|
155
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
156
|
+
),
|
157
|
+
MixerNumberDescription(
|
158
|
+
name="night_target_temp",
|
159
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
160
|
+
),
|
161
|
+
MixerNumberDescription(
|
162
|
+
name="min_target_temp",
|
163
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
164
|
+
),
|
165
|
+
MixerNumberDescription(
|
166
|
+
name="max_target_temp",
|
167
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
168
|
+
),
|
169
|
+
MixerSwitchDescription(
|
170
|
+
name="summer_work",
|
171
|
+
),
|
172
|
+
MixerSwitchDescription(
|
173
|
+
name="weather_control",
|
174
|
+
),
|
175
|
+
MixerNumberDescription(
|
176
|
+
name="enable_circuit",
|
177
|
+
),
|
178
|
+
MixerNumberDescription(
|
179
|
+
name="constant_water_preset_temp",
|
180
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
181
|
+
),
|
182
|
+
MixerNumberDescription(
|
183
|
+
name="thermostat_decrease_temp",
|
184
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
185
|
+
),
|
186
|
+
MixerNumberDescription(
|
187
|
+
name="thermostat_correction",
|
188
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
189
|
+
),
|
190
|
+
MixerSwitchDescription(
|
191
|
+
name="thermostat_pump_lock",
|
192
|
+
),
|
193
|
+
MixerNumberDescription(
|
194
|
+
name="valve_opening_time",
|
195
|
+
unit_of_measurement=UnitOfMeasurement.SECONDS,
|
196
|
+
),
|
197
|
+
MixerNumberDescription(
|
198
|
+
name="mixer_input_dead_zone",
|
199
|
+
step=0.1,
|
200
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
201
|
+
),
|
202
|
+
MixerNumberDescription(
|
203
|
+
name="proportional_range",
|
204
|
+
),
|
205
|
+
MixerNumberDescription(
|
206
|
+
name="integration_time_constant",
|
207
|
+
),
|
208
|
+
MixerNumberDescription(
|
209
|
+
name="heating_curve",
|
210
|
+
step=0.1,
|
211
|
+
),
|
212
|
+
MixerNumberDescription(
|
213
|
+
name="heating_curve_shift",
|
214
|
+
offset=20,
|
215
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
216
|
+
),
|
217
|
+
MixerNumberDescription(
|
218
|
+
name="thermostat_mode",
|
219
|
+
),
|
220
|
+
MixerNumberDescription(
|
221
|
+
name="decreasing_constant_water_temp",
|
222
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
223
|
+
),
|
224
|
+
],
|
225
|
+
}
|
226
|
+
|
227
|
+
|
228
|
+
@cache
|
229
|
+
def get_mixer_parameter_types(
|
230
|
+
product_info: ProductInfo,
|
231
|
+
) -> list[MixerParameterDescription]:
|
232
|
+
"""Return cached mixer parameter types for specific product."""
|
233
|
+
return PARAMETER_TYPES[product_info.type]
|
234
|
+
|
235
|
+
|
236
|
+
__all__ = [
|
237
|
+
"get_mixer_parameter_types",
|
238
|
+
"MixerNumber",
|
239
|
+
"MixerNumberDescription",
|
240
|
+
"MixerParameter",
|
241
|
+
"MixerParameterDescription",
|
242
|
+
"MixerSwitch",
|
243
|
+
"MixerSwitchDescription",
|
244
|
+
"PARAMETER_TYPES",
|
245
|
+
]
|
@@ -0,0 +1,197 @@
|
|
1
|
+
"""Contains thermostat parameter descriptors."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from functools import cache
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from dataslots import dataslots
|
10
|
+
|
11
|
+
from pyplumio.const import (
|
12
|
+
ATTR_INDEX,
|
13
|
+
ATTR_OFFSET,
|
14
|
+
ATTR_SIZE,
|
15
|
+
ATTR_VALUE,
|
16
|
+
FrameType,
|
17
|
+
UnitOfMeasurement,
|
18
|
+
)
|
19
|
+
from pyplumio.frames import Request
|
20
|
+
from pyplumio.parameters import (
|
21
|
+
Number,
|
22
|
+
NumberDescription,
|
23
|
+
Parameter,
|
24
|
+
ParameterDescription,
|
25
|
+
ParameterValues,
|
26
|
+
Switch,
|
27
|
+
SwitchDescription,
|
28
|
+
)
|
29
|
+
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from pyplumio.devices.thermostat import Thermostat
|
32
|
+
|
33
|
+
|
34
|
+
@dataclass
|
35
|
+
class ThermostatParameterDescription(ParameterDescription):
|
36
|
+
"""Represents a thermostat parameter description."""
|
37
|
+
|
38
|
+
__slots__ = ()
|
39
|
+
|
40
|
+
size: int = 1
|
41
|
+
|
42
|
+
|
43
|
+
class ThermostatParameter(Parameter):
|
44
|
+
"""Represents a thermostat parameter."""
|
45
|
+
|
46
|
+
__slots__ = ("offset",)
|
47
|
+
|
48
|
+
device: Thermostat
|
49
|
+
description: ThermostatParameterDescription
|
50
|
+
offset: int
|
51
|
+
|
52
|
+
def __init__(
|
53
|
+
self,
|
54
|
+
device: Thermostat,
|
55
|
+
description: ThermostatParameterDescription,
|
56
|
+
values: ParameterValues | None = None,
|
57
|
+
index: int = 0,
|
58
|
+
offset: int = 0,
|
59
|
+
) -> None:
|
60
|
+
"""Initialize a new thermostat parameter."""
|
61
|
+
self.offset = offset
|
62
|
+
super().__init__(device, description, values, index)
|
63
|
+
|
64
|
+
async def create_request(self) -> Request:
|
65
|
+
"""Create a request to change the parameter."""
|
66
|
+
return await Request.create(
|
67
|
+
FrameType.REQUEST_SET_THERMOSTAT_PARAMETER,
|
68
|
+
recipient=self.device.parent.address,
|
69
|
+
data={
|
70
|
+
# Increase the index by one to account for thermostat
|
71
|
+
# profile, which is being set at ecoMAX device level.
|
72
|
+
ATTR_INDEX: self._index + 1,
|
73
|
+
ATTR_VALUE: self.values.value,
|
74
|
+
ATTR_OFFSET: self.offset,
|
75
|
+
ATTR_SIZE: self.description.size,
|
76
|
+
},
|
77
|
+
)
|
78
|
+
|
79
|
+
|
80
|
+
@dataslots
|
81
|
+
@dataclass
|
82
|
+
class ThermostatNumberDescription(ThermostatParameterDescription, NumberDescription):
|
83
|
+
"""Represent a thermostat number description."""
|
84
|
+
|
85
|
+
|
86
|
+
class ThermostatNumber(ThermostatParameter, Number):
|
87
|
+
"""Represents a thermostat number."""
|
88
|
+
|
89
|
+
__slots__ = ()
|
90
|
+
|
91
|
+
description: ThermostatNumberDescription
|
92
|
+
|
93
|
+
|
94
|
+
@dataslots
|
95
|
+
@dataclass
|
96
|
+
class ThermostatSwitchDescription(ThermostatParameterDescription, SwitchDescription):
|
97
|
+
"""Represents a thermostat switch description."""
|
98
|
+
|
99
|
+
|
100
|
+
class ThermostatSwitch(ThermostatParameter, Switch):
|
101
|
+
"""Represents a thermostat switch."""
|
102
|
+
|
103
|
+
__slots__ = ()
|
104
|
+
|
105
|
+
description: ThermostatSwitchDescription
|
106
|
+
|
107
|
+
|
108
|
+
PARAMETER_TYPES: list[ThermostatParameterDescription] = [
|
109
|
+
ThermostatNumberDescription(
|
110
|
+
name="mode",
|
111
|
+
),
|
112
|
+
ThermostatNumberDescription(
|
113
|
+
name="party_target_temp",
|
114
|
+
size=2,
|
115
|
+
step=0.1,
|
116
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
117
|
+
),
|
118
|
+
ThermostatNumberDescription(
|
119
|
+
name="holidays_target_temp",
|
120
|
+
size=2,
|
121
|
+
step=0.1,
|
122
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
123
|
+
),
|
124
|
+
ThermostatNumberDescription(
|
125
|
+
name="correction",
|
126
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
127
|
+
),
|
128
|
+
ThermostatNumberDescription(
|
129
|
+
name="away_timer",
|
130
|
+
unit_of_measurement=UnitOfMeasurement.DAYS,
|
131
|
+
),
|
132
|
+
ThermostatNumberDescription(
|
133
|
+
name="airing_timer",
|
134
|
+
unit_of_measurement=UnitOfMeasurement.DAYS,
|
135
|
+
),
|
136
|
+
ThermostatNumberDescription(
|
137
|
+
name="party_timer",
|
138
|
+
unit_of_measurement=UnitOfMeasurement.DAYS,
|
139
|
+
),
|
140
|
+
ThermostatNumberDescription(
|
141
|
+
name="holidays_timer",
|
142
|
+
unit_of_measurement=UnitOfMeasurement.DAYS,
|
143
|
+
),
|
144
|
+
ThermostatNumberDescription(
|
145
|
+
name="hysteresis",
|
146
|
+
step=0.1,
|
147
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
148
|
+
),
|
149
|
+
ThermostatNumberDescription(
|
150
|
+
name="day_target_temp",
|
151
|
+
size=2,
|
152
|
+
step=0.1,
|
153
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
154
|
+
),
|
155
|
+
ThermostatNumberDescription(
|
156
|
+
name="night_target_temp",
|
157
|
+
size=2,
|
158
|
+
step=0.1,
|
159
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
160
|
+
),
|
161
|
+
ThermostatNumberDescription(
|
162
|
+
name="antifreeze_target_temp",
|
163
|
+
size=2,
|
164
|
+
step=0.1,
|
165
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
166
|
+
),
|
167
|
+
ThermostatNumberDescription(
|
168
|
+
name="heating_target_temp",
|
169
|
+
size=2,
|
170
|
+
step=0.1,
|
171
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
172
|
+
),
|
173
|
+
ThermostatNumberDescription(
|
174
|
+
name="heating_timer",
|
175
|
+
),
|
176
|
+
ThermostatNumberDescription(
|
177
|
+
name="off_timer",
|
178
|
+
),
|
179
|
+
]
|
180
|
+
|
181
|
+
|
182
|
+
@cache
|
183
|
+
def get_thermostat_parameter_types() -> list[ThermostatParameterDescription]:
|
184
|
+
"""Return cached thermostat parameter types for specific product."""
|
185
|
+
return PARAMETER_TYPES
|
186
|
+
|
187
|
+
|
188
|
+
__all__ = [
|
189
|
+
"get_thermostat_parameter_types",
|
190
|
+
"PARAMETER_TYPES",
|
191
|
+
"ThermostatNumber",
|
192
|
+
"ThermostatNumberDescription",
|
193
|
+
"ThermostatParameter",
|
194
|
+
"ThermostatParameterDescription",
|
195
|
+
"ThermostatSwitch",
|
196
|
+
"ThermostatSwitchDescription",
|
197
|
+
]
|
pyplumio/protocol.py
CHANGED
@@ -25,7 +25,7 @@ from pyplumio.structures.network_info import (
|
|
25
25
|
|
26
26
|
_LOGGER = logging.getLogger(__name__)
|
27
27
|
|
28
|
-
|
28
|
+
Callback: TypeAlias = Callable[[], Awaitable[None]]
|
29
29
|
|
30
30
|
|
31
31
|
class Protocol(ABC):
|
@@ -34,7 +34,7 @@ class Protocol(ABC):
|
|
34
34
|
connected: asyncio.Event
|
35
35
|
reader: FrameReader | None
|
36
36
|
writer: FrameWriter | None
|
37
|
-
_on_connection_lost: set[
|
37
|
+
_on_connection_lost: set[Callback]
|
38
38
|
|
39
39
|
def __init__(self) -> None:
|
40
40
|
"""Initialize a new protocol."""
|
@@ -51,7 +51,7 @@ class Protocol(ABC):
|
|
51
51
|
self.writer = None
|
52
52
|
|
53
53
|
@property
|
54
|
-
def on_connection_lost(self) -> set[
|
54
|
+
def on_connection_lost(self) -> set[Callback]:
|
55
55
|
"""Return the callbacks that'll be called on connection lost."""
|
56
56
|
return self._on_connection_lost
|
57
57
|
|
@@ -236,3 +236,6 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
236
236
|
await self.dispatch(name, device)
|
237
237
|
|
238
238
|
return self.data[name]
|
239
|
+
|
240
|
+
|
241
|
+
__all__ = ["Protocol", "DummyProtocol", "AsyncProtocol"]
|
pyplumio/stream.py
CHANGED
pyplumio/structures/__init__.py
CHANGED
@@ -51,3 +51,6 @@ class StructureDecoder(Structure, ABC):
|
|
51
51
|
self, message: bytearray, offset: int = 0, data: dict[str, Any] | None = None
|
52
52
|
) -> tuple[dict[str, Any], int]:
|
53
53
|
"""Decode bytes and return message data and offset."""
|
54
|
+
|
55
|
+
|
56
|
+
__all__ = ["Structure", "StructureDecoder"]
|
pyplumio/structures/alerts.py
CHANGED
@@ -39,21 +39,21 @@ DATETIME_INTERVALS: tuple[DateTimeInterval, ...] = (
|
|
39
39
|
|
40
40
|
|
41
41
|
@lru_cache(maxsize=10)
|
42
|
-
def
|
42
|
+
def seconds_to_datetime(timestamp: int) -> datetime:
|
43
43
|
"""Convert timestamp to a datetime object.
|
44
44
|
|
45
45
|
The ecoMAX controller stores alert time as a special timestamp value
|
46
46
|
in seconds counted from Jan 1st, 2000.
|
47
47
|
"""
|
48
48
|
|
49
|
-
def
|
49
|
+
def datetime_kwargs(timestamp: int) -> Generator[Any, None, None]:
|
50
50
|
"""Yield a tuple, that represents a single datetime kwarg."""
|
51
51
|
for name, seconds, offset in DATETIME_INTERVALS:
|
52
52
|
value = timestamp // seconds
|
53
53
|
timestamp -= value * seconds
|
54
54
|
yield name, (value + offset)
|
55
55
|
|
56
|
-
return datetime(**dict(
|
56
|
+
return datetime(**dict(datetime_kwargs(timestamp)))
|
57
57
|
|
58
58
|
|
59
59
|
@dataclass
|
@@ -83,11 +83,11 @@ class AlertsStructure(StructureDecoder):
|
|
83
83
|
offset += from_seconds.size
|
84
84
|
to_seconds = UnsignedInt.from_bytes(message, offset)
|
85
85
|
offset += to_seconds.size
|
86
|
-
from_dt =
|
86
|
+
from_dt = seconds_to_datetime(from_seconds.value)
|
87
87
|
to_dt = (
|
88
88
|
None
|
89
89
|
if to_seconds.value == MAX_UINT32
|
90
|
-
else
|
90
|
+
else seconds_to_datetime(to_seconds.value)
|
91
91
|
)
|
92
92
|
with suppress(ValueError):
|
93
93
|
code = AlertType(code)
|
@@ -119,3 +119,6 @@ class AlertsStructure(StructureDecoder):
|
|
119
119
|
),
|
120
120
|
self._offset,
|
121
121
|
)
|
122
|
+
|
123
|
+
|
124
|
+
__all__ = ["AlertsStructure", "Alert"]
|