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
@@ -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
@@ -10,11 +10,12 @@ import logging
|
|
10
10
|
|
11
11
|
from typing_extensions import TypeAlias
|
12
12
|
|
13
|
-
from pyplumio.const import ATTR_CONNECTED, DeviceType
|
13
|
+
from pyplumio.const import ATTR_CONNECTED, ATTR_SETUP, DeviceType
|
14
14
|
from pyplumio.devices import PhysicalDevice
|
15
15
|
from pyplumio.exceptions import ProtocolError
|
16
16
|
from pyplumio.frames import Frame
|
17
17
|
from pyplumio.frames.requests import StartMasterRequest
|
18
|
+
from pyplumio.helpers.async_cache import acache
|
18
19
|
from pyplumio.helpers.event_manager import EventManager
|
19
20
|
from pyplumio.stream import FrameReader, FrameWriter
|
20
21
|
from pyplumio.structures.network_info import (
|
@@ -25,7 +26,7 @@ from pyplumio.structures.network_info import (
|
|
25
26
|
|
26
27
|
_LOGGER = logging.getLogger(__name__)
|
27
28
|
|
28
|
-
|
29
|
+
Callback: TypeAlias = Callable[[], Awaitable[None]]
|
29
30
|
|
30
31
|
|
31
32
|
class Protocol(ABC):
|
@@ -34,7 +35,7 @@ class Protocol(ABC):
|
|
34
35
|
connected: asyncio.Event
|
35
36
|
reader: FrameReader | None
|
36
37
|
writer: FrameWriter | None
|
37
|
-
_on_connection_lost: set[
|
38
|
+
_on_connection_lost: set[Callback]
|
38
39
|
|
39
40
|
def __init__(self) -> None:
|
40
41
|
"""Initialize a new protocol."""
|
@@ -51,7 +52,7 @@ class Protocol(ABC):
|
|
51
52
|
self.writer = None
|
52
53
|
|
53
54
|
@property
|
54
|
-
def on_connection_lost(self) -> set[
|
55
|
+
def on_connection_lost(self) -> set[Callback]:
|
55
56
|
"""Return the callbacks that'll be called on connection lost."""
|
56
57
|
return self._on_connection_lost
|
57
58
|
|
@@ -132,6 +133,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
132
133
|
consumers_count: int
|
133
134
|
_network: NetworkInfo
|
134
135
|
_queues: Queues
|
136
|
+
_entry_lock: asyncio.Lock
|
135
137
|
|
136
138
|
def __init__(
|
137
139
|
self,
|
@@ -147,6 +149,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
147
149
|
wlan=wireless_parameters or WirelessParameters(status=False),
|
148
150
|
)
|
149
151
|
self._queues = Queues(read=asyncio.Queue(), write=asyncio.Queue())
|
152
|
+
self._entry_lock = asyncio.Lock()
|
150
153
|
|
151
154
|
def connection_established(
|
152
155
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
@@ -224,15 +227,23 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
224
227
|
device.handle_frame(frame)
|
225
228
|
queue.task_done()
|
226
229
|
|
230
|
+
@acache
|
227
231
|
async def get_device_entry(self, device_type: DeviceType) -> PhysicalDevice:
|
228
|
-
"""
|
229
|
-
|
230
|
-
|
232
|
+
"""Return the device entry."""
|
233
|
+
|
234
|
+
@acache
|
235
|
+
async def _setup_device_entry(device_type: DeviceType) -> PhysicalDevice:
|
236
|
+
"""Set up the device entry."""
|
231
237
|
device = await PhysicalDevice.create(
|
232
238
|
device_type, queue=self._queues.write, network=self._network
|
233
239
|
)
|
234
240
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
235
|
-
|
236
|
-
|
241
|
+
device.dispatch_nowait(ATTR_SETUP, True)
|
242
|
+
self.dispatch_nowait(device_type.name.lower(), device)
|
243
|
+
return device
|
244
|
+
|
245
|
+
async with self._entry_lock:
|
246
|
+
return await _setup_device_entry(device_type)
|
247
|
+
|
237
248
|
|
238
|
-
|
249
|
+
__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
@@ -10,7 +10,7 @@ from functools import lru_cache
|
|
10
10
|
from typing import Any, Final, Literal, NamedTuple
|
11
11
|
|
12
12
|
from pyplumio.const import AlertType
|
13
|
-
from pyplumio.
|
13
|
+
from pyplumio.data_types import UnsignedInt
|
14
14
|
from pyplumio.structures import StructureDecoder
|
15
15
|
from pyplumio.utils import ensure_dict
|
16
16
|
|
@@ -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"]
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import math
|
6
6
|
from typing import Any, Final
|
7
7
|
|
8
|
-
from pyplumio.
|
8
|
+
from pyplumio.data_types import Float
|
9
9
|
from pyplumio.structures import StructureDecoder
|
10
10
|
from pyplumio.utils import ensure_dict
|
11
11
|
|
@@ -28,3 +28,6 @@ class BoilerPowerStructure(StructureDecoder):
|
|
28
28
|
return ensure_dict(data), offset
|
29
29
|
|
30
30
|
return ensure_dict(data, {ATTR_BOILER_POWER: boiler_power.value}), offset
|
31
|
+
|
32
|
+
|
33
|
+
__all__ = ["ATTR_BOILER_POWER", "BoilerPowerStructure"]
|