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.
Files changed (60) hide show
  1. pyplumio/__init__.py +3 -2
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +14 -14
  4. pyplumio/const.py +7 -0
  5. pyplumio/devices/__init__.py +32 -19
  6. pyplumio/devices/ecomax.py +112 -128
  7. pyplumio/devices/ecoster.py +5 -0
  8. pyplumio/devices/mixer.py +21 -31
  9. pyplumio/devices/thermostat.py +19 -29
  10. pyplumio/filters.py +166 -147
  11. pyplumio/frames/__init__.py +20 -8
  12. pyplumio/frames/messages.py +3 -0
  13. pyplumio/frames/requests.py +21 -0
  14. pyplumio/frames/responses.py +18 -0
  15. pyplumio/helpers/data_types.py +23 -21
  16. pyplumio/helpers/event_manager.py +40 -3
  17. pyplumio/helpers/factory.py +5 -2
  18. pyplumio/helpers/schedule.py +8 -5
  19. pyplumio/helpers/task_manager.py +3 -0
  20. pyplumio/helpers/timeout.py +8 -8
  21. pyplumio/helpers/uid.py +8 -5
  22. pyplumio/{helpers/parameter.py → parameters/__init__.py} +98 -4
  23. pyplumio/parameters/ecomax.py +868 -0
  24. pyplumio/parameters/mixer.py +245 -0
  25. pyplumio/parameters/thermostat.py +197 -0
  26. pyplumio/protocol.py +6 -3
  27. pyplumio/stream.py +3 -0
  28. pyplumio/structures/__init__.py +3 -0
  29. pyplumio/structures/alerts.py +8 -5
  30. pyplumio/structures/boiler_load.py +3 -0
  31. pyplumio/structures/boiler_power.py +3 -0
  32. pyplumio/structures/ecomax_parameters.py +6 -800
  33. pyplumio/structures/fan_power.py +3 -0
  34. pyplumio/structures/frame_versions.py +3 -0
  35. pyplumio/structures/fuel_consumption.py +3 -0
  36. pyplumio/structures/fuel_level.py +3 -0
  37. pyplumio/structures/lambda_sensor.py +8 -0
  38. pyplumio/structures/mixer_parameters.py +8 -230
  39. pyplumio/structures/mixer_sensors.py +9 -0
  40. pyplumio/structures/modules.py +14 -0
  41. pyplumio/structures/network_info.py +11 -0
  42. pyplumio/structures/output_flags.py +9 -0
  43. pyplumio/structures/outputs.py +21 -0
  44. pyplumio/structures/pending_alerts.py +3 -0
  45. pyplumio/structures/product_info.py +5 -2
  46. pyplumio/structures/program_version.py +3 -0
  47. pyplumio/structures/regulator_data.py +4 -1
  48. pyplumio/structures/regulator_data_schema.py +3 -0
  49. pyplumio/structures/schedules.py +18 -1
  50. pyplumio/structures/statuses.py +9 -0
  51. pyplumio/structures/temperatures.py +22 -0
  52. pyplumio/structures/thermostat_parameters.py +13 -177
  53. pyplumio/structures/thermostat_sensors.py +9 -0
  54. pyplumio/utils.py +14 -12
  55. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/METADATA +30 -17
  56. pyplumio-0.5.43.dist-info/RECORD +63 -0
  57. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/WHEEL +1 -1
  58. pyplumio-0.5.42.dist-info/RECORD +0 -60
  59. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/licenses/LICENSE +0 -0
  60. {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
- _Callback: TypeAlias = Callable[[], Awaitable[None]]
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[_Callback]
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[_Callback]:
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
@@ -153,3 +153,6 @@ class FrameReader:
153
153
  _LOGGER.debug("Received frame: %s, bytes: %s", frame, buffer.hex())
154
154
 
155
155
  return frame
156
+
157
+
158
+ __all__ = ["FrameReader", "FrameWriter"]
@@ -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"]
@@ -39,21 +39,21 @@ DATETIME_INTERVALS: tuple[DateTimeInterval, ...] = (
39
39
 
40
40
 
41
41
  @lru_cache(maxsize=10)
42
- def _seconds_to_datetime(timestamp: int) -> datetime:
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 _datetime_kwargs(timestamp: int) -> Generator[Any, None, None]:
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(_datetime_kwargs(timestamp)))
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 = _seconds_to_datetime(from_seconds.value)
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 _seconds_to_datetime(to_seconds.value)
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"]
@@ -27,3 +27,6 @@ class BoilerLoadStructure(StructureDecoder):
27
27
  return ensure_dict(data), offset
28
28
 
29
29
  return (ensure_dict(data, {ATTR_BOILER_LOAD: boiler_load}), offset)
30
+
31
+
32
+ __all__ = ["ATTR_BOILER_LOAD", "BoilerLoadStructure"]
@@ -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"]