PyPlumIO 0.5.43__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/_version.py +2 -2
- pyplumio/const.py +1 -3
- pyplumio/devices/__init__.py +16 -28
- pyplumio/devices/ecomax.py +103 -59
- pyplumio/devices/mixer.py +6 -6
- pyplumio/devices/thermostat.py +9 -6
- pyplumio/filters.py +44 -22
- pyplumio/helpers/async_cache.py +48 -0
- pyplumio/helpers/event_manager.py +20 -2
- pyplumio/helpers/timeout.py +6 -5
- pyplumio/parameters/__init__.py +9 -3
- pyplumio/protocol.py +15 -7
- pyplumio/structures/alerts.py +1 -1
- pyplumio/structures/boiler_power.py +1 -1
- pyplumio/structures/fan_power.py +1 -1
- pyplumio/structures/frame_versions.py +1 -1
- pyplumio/structures/fuel_consumption.py +1 -1
- pyplumio/structures/lambda_sensor.py +1 -1
- pyplumio/structures/mixer_sensors.py +1 -1
- pyplumio/structures/network_info.py +1 -1
- pyplumio/structures/output_flags.py +1 -1
- pyplumio/structures/outputs.py +1 -1
- pyplumio/structures/product_info.py +1 -1
- pyplumio/structures/regulator_data.py +1 -1
- pyplumio/structures/regulator_data_schema.py +1 -1
- pyplumio/structures/temperatures.py +1 -1
- pyplumio/structures/thermostat_parameters.py +5 -7
- pyplumio/structures/thermostat_sensors.py +1 -1
- {pyplumio-0.5.43.dist-info → pyplumio-0.5.44.dist-info}/METADATA +3 -1
- pyplumio-0.5.44.dist-info/RECORD +64 -0
- {pyplumio-0.5.43.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
- pyplumio-0.5.43.dist-info/RECORD +0 -63
- /pyplumio/{helpers/data_types.py → data_types.py} +0 -0
- {pyplumio-0.5.43.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.43.dist-info → pyplumio-0.5.44.dist-info}/top_level.txt +0 -0
pyplumio/_version.py
CHANGED
pyplumio/const.py
CHANGED
@@ -14,11 +14,11 @@ ATTR_CURRENT_TEMP: Final = "current_temp"
|
|
14
14
|
ATTR_DEVICE_INDEX: Final = "device_index"
|
15
15
|
ATTR_FRAME_ERRORS: Final = "frame_errors"
|
16
16
|
ATTR_INDEX: Final = "index"
|
17
|
-
ATTR_LOADED: Final = "loaded"
|
18
17
|
ATTR_OFFSET: Final = "offset"
|
19
18
|
ATTR_PARAMETER: Final = "parameter"
|
20
19
|
ATTR_PASSWORD: Final = "password"
|
21
20
|
ATTR_SCHEDULE: Final = "schedule"
|
21
|
+
ATTR_SETUP: Final = "setup"
|
22
22
|
ATTR_SENSORS: Final = "sensors"
|
23
23
|
ATTR_START: Final = "start"
|
24
24
|
ATTR_STATE: Final = "state"
|
@@ -229,6 +229,4 @@ PERCENTAGE: Final = "%"
|
|
229
229
|
|
230
230
|
STATE_ON: Final = "on"
|
231
231
|
STATE_OFF: Final = "off"
|
232
|
-
|
233
|
-
|
234
232
|
State: TypeAlias = Literal["on", "off"]
|
pyplumio/devices/__init__.py
CHANGED
@@ -8,14 +8,13 @@ from functools import cache
|
|
8
8
|
import logging
|
9
9
|
from typing import Any, ClassVar
|
10
10
|
|
11
|
-
from pyplumio.const import ATTR_FRAME_ERRORS,
|
11
|
+
from pyplumio.const import ATTR_FRAME_ERRORS, DeviceType, FrameType, State
|
12
12
|
from pyplumio.exceptions import RequestError, UnknownDeviceError
|
13
13
|
from pyplumio.filters import on_change
|
14
|
-
from pyplumio.frames import
|
14
|
+
from pyplumio.frames import Frame, Request, is_known_frame_type
|
15
15
|
from pyplumio.helpers.event_manager import EventManager, event_listener
|
16
16
|
from pyplumio.helpers.factory import create_instance
|
17
17
|
from pyplumio.parameters import NumericType, Parameter
|
18
|
-
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
19
18
|
from pyplumio.structures.network_info import NetworkInfo
|
20
19
|
from pyplumio.utils import to_camelcase
|
21
20
|
|
@@ -126,11 +125,11 @@ class PhysicalDevice(Device, ABC):
|
|
126
125
|
virtual devices associated with them via parent property.
|
127
126
|
"""
|
128
127
|
|
129
|
-
__slots__ = ("address", "_network", "
|
128
|
+
__slots__ = ("address", "_network", "_frame_versions")
|
130
129
|
|
131
130
|
address: ClassVar[int]
|
131
|
+
|
132
132
|
_network: NetworkInfo
|
133
|
-
_setup_frames: tuple[DataFrameDescription, ...]
|
134
133
|
_frame_versions: dict[int, int]
|
135
134
|
|
136
135
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
@@ -139,20 +138,27 @@ class PhysicalDevice(Device, ABC):
|
|
139
138
|
self._network = network
|
140
139
|
self._frame_versions = {}
|
141
140
|
|
142
|
-
@event_listener(
|
141
|
+
@event_listener(filter=on_change)
|
143
142
|
async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
|
144
143
|
"""Check frame versions and update outdated frames."""
|
144
|
+
_LOGGER.info("Received version table")
|
145
145
|
for frame_type, version in versions.items():
|
146
146
|
if (
|
147
147
|
is_known_frame_type(frame_type)
|
148
148
|
and self.supports_frame_type(frame_type)
|
149
149
|
and not self.has_frame_version(frame_type, version)
|
150
150
|
):
|
151
|
-
|
152
|
-
request = await Request.create(frame_type, recipient=self.address)
|
153
|
-
self.queue.put_nowait(request)
|
151
|
+
await self._request_frame_version(frame_type, version)
|
154
152
|
self._frame_versions[frame_type] = version
|
155
153
|
|
154
|
+
async def _request_frame_version(
|
155
|
+
self, frame_type: FrameType | int, version: int
|
156
|
+
) -> None:
|
157
|
+
"""Request frame version from the device."""
|
158
|
+
_LOGGER.info("Updating frame %s to version %i", repr(frame_type), version)
|
159
|
+
request = await Request.create(frame_type, recipient=self.address)
|
160
|
+
self.queue.put_nowait(request)
|
161
|
+
|
156
162
|
def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
|
157
163
|
"""Return True if frame data is up to date, False otherwise."""
|
158
164
|
return (
|
@@ -171,25 +177,6 @@ class PhysicalDevice(Device, ABC):
|
|
171
177
|
for name, value in frame.data.items():
|
172
178
|
self.dispatch_nowait(name, value)
|
173
179
|
|
174
|
-
async def async_setup(self) -> bool:
|
175
|
-
"""Set up addressable device."""
|
176
|
-
results = await asyncio.gather(
|
177
|
-
*(
|
178
|
-
self.request(description.provides, description.frame_type)
|
179
|
-
for description in self._setup_frames
|
180
|
-
),
|
181
|
-
return_exceptions=True,
|
182
|
-
)
|
183
|
-
|
184
|
-
errors = [
|
185
|
-
result.frame_type for result in results if isinstance(result, RequestError)
|
186
|
-
]
|
187
|
-
|
188
|
-
await asyncio.gather(
|
189
|
-
self.dispatch(ATTR_FRAME_ERRORS, errors), self.dispatch(ATTR_LOADED, True)
|
190
|
-
)
|
191
|
-
return True
|
192
|
-
|
193
180
|
async def request(
|
194
181
|
self, name: str, frame_type: FrameType, retries: int = 3, timeout: float = 3.0
|
195
182
|
) -> Any:
|
@@ -197,6 +184,7 @@ class PhysicalDevice(Device, ABC):
|
|
197
184
|
|
198
185
|
If value is not available before timeout, retry request.
|
199
186
|
"""
|
187
|
+
_LOGGER.info("Requesting '%s' with %s", name, repr(frame_type))
|
200
188
|
request = await Request.create(frame_type, recipient=self.address)
|
201
189
|
while retries > 0:
|
202
190
|
try:
|
pyplumio/devices/ecomax.py
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Coroutine, Generator, Iterable
|
6
|
+
from collections.abc import Coroutine, Generator, Iterable
|
7
7
|
import logging
|
8
8
|
import time
|
9
9
|
from typing import Any, Final
|
10
10
|
|
11
11
|
from pyplumio.const import (
|
12
|
+
ATTR_FRAME_ERRORS,
|
12
13
|
ATTR_PASSWORD,
|
13
14
|
ATTR_SENSORS,
|
14
|
-
ATTR_STATE,
|
15
15
|
STATE_OFF,
|
16
16
|
STATE_ON,
|
17
17
|
DeviceState,
|
@@ -22,6 +22,7 @@ from pyplumio.const import (
|
|
22
22
|
from pyplumio.devices import PhysicalDevice
|
23
23
|
from pyplumio.devices.mixer import Mixer
|
24
24
|
from pyplumio.devices.thermostat import Thermostat
|
25
|
+
from pyplumio.exceptions import RequestError
|
25
26
|
from pyplumio.filters import on_change
|
26
27
|
from pyplumio.frames import DataFrameDescription, Frame, Request
|
27
28
|
from pyplumio.helpers.event_manager import event_listener
|
@@ -40,14 +41,12 @@ from pyplumio.structures.ecomax_parameters import (
|
|
40
41
|
ATTR_ECOMAX_CONTROL,
|
41
42
|
ATTR_ECOMAX_PARAMETERS,
|
42
43
|
)
|
43
|
-
from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
|
44
44
|
from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
|
45
45
|
from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
|
46
46
|
from pyplumio.structures.network_info import ATTR_NETWORK, NetworkInfo
|
47
47
|
from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
|
48
48
|
from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
|
49
49
|
from pyplumio.structures.schedules import (
|
50
|
-
ATTR_SCHEDULE_PARAMETERS,
|
51
50
|
ATTR_SCHEDULES,
|
52
51
|
SCHEDULE_PARAMETERS,
|
53
52
|
SCHEDULES,
|
@@ -55,20 +54,53 @@ from pyplumio.structures.schedules import (
|
|
55
54
|
ScheduleSwitch,
|
56
55
|
ScheduleSwitchDescription,
|
57
56
|
)
|
58
|
-
from pyplumio.structures.thermostat_parameters import
|
59
|
-
ATTR_THERMOSTAT_PARAMETERS,
|
60
|
-
ATTR_THERMOSTAT_PROFILE,
|
61
|
-
)
|
57
|
+
from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
|
62
58
|
from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
|
63
59
|
|
60
|
+
_LOGGER = logging.getLogger(__name__)
|
61
|
+
|
62
|
+
|
64
63
|
ATTR_MIXERS: Final = "mixers"
|
65
64
|
ATTR_THERMOSTATS: Final = "thermostats"
|
66
65
|
ATTR_FUEL_BURNED: Final = "fuel_burned"
|
67
66
|
|
68
|
-
|
69
|
-
|
67
|
+
MAX_TIME_SINCE_LAST_FUEL_UPDATE: Final = 5 * 60
|
68
|
+
|
69
|
+
|
70
|
+
class FuelMeter:
|
71
|
+
"""Represents a fuel meter.
|
72
|
+
|
73
|
+
Calculates the fuel burned based on the time
|
74
|
+
elapsed since the last sensor message, which contains fuel
|
75
|
+
consumption data. If the elapsed time is within the acceptable
|
76
|
+
range, it returns the fuel burned data. Otherwise, it logs a
|
77
|
+
warning and returns None.
|
78
|
+
"""
|
79
|
+
|
80
|
+
__slots__ = ("_last_update_time",)
|
81
|
+
|
82
|
+
_last_update_time: float
|
70
83
|
|
71
|
-
|
84
|
+
def __init__(self) -> None:
|
85
|
+
"""Initialize a new fuel meter."""
|
86
|
+
self._last_update_time = time.monotonic()
|
87
|
+
|
88
|
+
def calculate(self, fuel_consumption: float) -> float | None:
|
89
|
+
"""Calculate the amount of burned fuel since last update."""
|
90
|
+
current_time = time.monotonic()
|
91
|
+
time_since_update = current_time - self._last_update_time
|
92
|
+
self._last_update_time = current_time
|
93
|
+
if time_since_update < MAX_TIME_SINCE_LAST_FUEL_UPDATE:
|
94
|
+
return fuel_consumption * (time_since_update / 3600)
|
95
|
+
|
96
|
+
_LOGGER.warning(
|
97
|
+
"Skipping outdated fuel consumption data (was %f seconds old)",
|
98
|
+
time_since_update,
|
99
|
+
)
|
100
|
+
return None
|
101
|
+
|
102
|
+
|
103
|
+
REQUIRED: tuple[DataFrameDescription, ...] = (
|
72
104
|
DataFrameDescription(
|
73
105
|
frame_type=FrameType.REQUEST_UID,
|
74
106
|
provides=ATTR_PRODUCT,
|
@@ -103,28 +135,22 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
|
103
135
|
),
|
104
136
|
)
|
105
137
|
|
106
|
-
|
138
|
+
REQUIRED_TYPES = [description.frame_type for description in REQUIRED]
|
107
139
|
|
108
140
|
|
109
141
|
class EcoMAX(PhysicalDevice):
|
110
142
|
"""Represents an ecoMAX controller."""
|
111
143
|
|
112
|
-
__slots__ = ("
|
144
|
+
__slots__ = ("_fuel_meter",)
|
113
145
|
|
114
|
-
|
115
|
-
_setup_frames = SETUP_FRAME_TYPES
|
146
|
+
_fuel_meter: FuelMeter
|
116
147
|
|
117
|
-
|
148
|
+
address = DeviceType.ECOMAX
|
118
149
|
|
119
150
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
120
151
|
"""Initialize a new ecoMAX controller."""
|
121
152
|
super().__init__(queue, network)
|
122
|
-
self.
|
123
|
-
|
124
|
-
async def async_setup(self) -> bool:
|
125
|
-
"""Set up an ecoMAX controller."""
|
126
|
-
await self.wait_for(ATTR_SENSORS)
|
127
|
-
return await super().async_setup()
|
153
|
+
self._fuel_meter = FuelMeter()
|
128
154
|
|
129
155
|
def handle_frame(self, frame: Frame) -> None:
|
130
156
|
"""Handle frame received from the ecoMAX device."""
|
@@ -162,6 +188,13 @@ class EcoMAX(PhysicalDevice):
|
|
162
188
|
|
163
189
|
return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
|
164
190
|
|
191
|
+
async def _request_frame_version(
|
192
|
+
self, frame_type: FrameType | int, version: int
|
193
|
+
) -> None:
|
194
|
+
"""Request frame version from the device."""
|
195
|
+
if frame_type not in REQUIRED_TYPES:
|
196
|
+
await super()._request_frame_version(frame_type, version)
|
197
|
+
|
165
198
|
async def _set_ecomax_state(self, state: State) -> bool:
|
166
199
|
"""Try to set the ecoMAX control state."""
|
167
200
|
try:
|
@@ -196,11 +229,34 @@ class EcoMAX(PhysicalDevice):
|
|
196
229
|
await asyncio.gather(*(device.shutdown() for device in devices))
|
197
230
|
await super().shutdown()
|
198
231
|
|
199
|
-
@event_listener
|
232
|
+
@event_listener
|
233
|
+
async def on_event_setup(self, setup: bool) -> None:
|
234
|
+
"""Request frames required to set up an ecoMAX entry."""
|
235
|
+
_LOGGER.info("Setting up device entry")
|
236
|
+
await self.wait_for(ATTR_SENSORS)
|
237
|
+
results = await asyncio.gather(
|
238
|
+
*(
|
239
|
+
self.request(description.provides, description.frame_type)
|
240
|
+
for description in REQUIRED
|
241
|
+
),
|
242
|
+
return_exceptions=True,
|
243
|
+
)
|
244
|
+
|
245
|
+
errors = [
|
246
|
+
result.frame_type for result in results if isinstance(result, RequestError)
|
247
|
+
]
|
248
|
+
|
249
|
+
if errors:
|
250
|
+
self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
|
251
|
+
|
252
|
+
_LOGGER.info("Device entry setup done")
|
253
|
+
|
254
|
+
@event_listener
|
200
255
|
async def on_event_ecomax_parameters(
|
201
|
-
self, parameters:
|
256
|
+
self, parameters: list[tuple[int, ParameterValues]]
|
202
257
|
) -> bool:
|
203
258
|
"""Update ecoMAX parameters and dispatch the events."""
|
259
|
+
_LOGGER.info("Received device parameters")
|
204
260
|
product_info: ProductInfo = await self.get(ATTR_PRODUCT)
|
205
261
|
|
206
262
|
def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
|
@@ -236,37 +292,20 @@ class EcoMAX(PhysicalDevice):
|
|
236
292
|
await asyncio.gather(*_ecomax_parameter_events())
|
237
293
|
return True
|
238
294
|
|
239
|
-
@event_listener
|
295
|
+
@event_listener
|
240
296
|
async def on_event_fuel_consumption(self, fuel_consumption: float) -> None:
|
241
|
-
"""Update the amount of burned fuel.
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
consumption data. If the elapsed time is within the acceptable
|
246
|
-
range, it dispatches the fuel burned data. Otherwise, it logs a
|
247
|
-
warning and skips the outdated data.
|
248
|
-
"""
|
249
|
-
time_ns = time.perf_counter_ns()
|
250
|
-
nanoseconds_passed = time_ns - self._fuel_burned_time_ns
|
251
|
-
self._fuel_burned_time_ns = time_ns
|
252
|
-
if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
|
253
|
-
return self.dispatch_nowait(
|
254
|
-
ATTR_FUEL_BURNED,
|
255
|
-
fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
|
256
|
-
)
|
257
|
-
|
258
|
-
_LOGGER.warning(
|
259
|
-
"Skipping outdated fuel consumption data: %f (was %i seconds old)",
|
260
|
-
fuel_consumption,
|
261
|
-
nanoseconds_passed / NANOSECONDS_IN_SECOND,
|
262
|
-
)
|
297
|
+
"""Update the amount of burned fuel."""
|
298
|
+
fuel_burned = self._fuel_meter.calculate(fuel_consumption)
|
299
|
+
if fuel_burned is not None:
|
300
|
+
self.dispatch_nowait(ATTR_FUEL_BURNED, fuel_burned)
|
263
301
|
|
264
|
-
@event_listener
|
302
|
+
@event_listener
|
265
303
|
async def on_event_mixer_parameters(
|
266
304
|
self,
|
267
|
-
parameters: dict[int,
|
305
|
+
parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
|
268
306
|
) -> bool:
|
269
307
|
"""Handle mixer parameters and dispatch the events."""
|
308
|
+
_LOGGER.info("Received mixer parameters")
|
270
309
|
if parameters:
|
271
310
|
await asyncio.gather(
|
272
311
|
*(
|
@@ -278,11 +317,12 @@ class EcoMAX(PhysicalDevice):
|
|
278
317
|
|
279
318
|
return False
|
280
319
|
|
281
|
-
@event_listener
|
320
|
+
@event_listener
|
282
321
|
async def on_event_mixer_sensors(
|
283
322
|
self, sensors: dict[int, dict[str, Any]] | None
|
284
323
|
) -> bool:
|
285
324
|
"""Update mixer sensors and dispatch the events."""
|
325
|
+
_LOGGER.info("Received mixer sensors")
|
286
326
|
if sensors:
|
287
327
|
await asyncio.gather(
|
288
328
|
*(
|
@@ -294,9 +334,9 @@ class EcoMAX(PhysicalDevice):
|
|
294
334
|
|
295
335
|
return False
|
296
336
|
|
297
|
-
@event_listener
|
337
|
+
@event_listener
|
298
338
|
async def on_event_schedule_parameters(
|
299
|
-
self, parameters:
|
339
|
+
self, parameters: list[tuple[int, ParameterValues]]
|
300
340
|
) -> bool:
|
301
341
|
"""Update schedule parameters and dispatch the events."""
|
302
342
|
|
@@ -319,20 +359,22 @@ class EcoMAX(PhysicalDevice):
|
|
319
359
|
await asyncio.gather(*_schedule_parameter_events())
|
320
360
|
return True
|
321
361
|
|
322
|
-
@event_listener
|
362
|
+
@event_listener
|
323
363
|
async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
|
324
364
|
"""Update ecoMAX sensors and dispatch the events."""
|
365
|
+
_LOGGER.info("Received device sensors")
|
325
366
|
await asyncio.gather(
|
326
367
|
*(self.dispatch(name, value) for name, value in sensors.items())
|
327
368
|
)
|
328
369
|
return True
|
329
370
|
|
330
|
-
@event_listener
|
371
|
+
@event_listener
|
331
372
|
async def on_event_thermostat_parameters(
|
332
373
|
self,
|
333
|
-
parameters: dict[int,
|
374
|
+
parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
|
334
375
|
) -> bool:
|
335
376
|
"""Handle thermostat parameters and dispatch the events."""
|
377
|
+
_LOGGER.info("Received thermostat parameters")
|
336
378
|
if parameters:
|
337
379
|
await asyncio.gather(
|
338
380
|
*(
|
@@ -346,7 +388,7 @@ class EcoMAX(PhysicalDevice):
|
|
346
388
|
|
347
389
|
return False
|
348
390
|
|
349
|
-
@event_listener
|
391
|
+
@event_listener
|
350
392
|
async def on_event_thermostat_profile(
|
351
393
|
self, values: ParameterValues | None
|
352
394
|
) -> EcomaxNumber | None:
|
@@ -358,11 +400,12 @@ class EcoMAX(PhysicalDevice):
|
|
358
400
|
|
359
401
|
return None
|
360
402
|
|
361
|
-
@event_listener
|
403
|
+
@event_listener
|
362
404
|
async def on_event_thermostat_sensors(
|
363
405
|
self, sensors: dict[int, dict[str, Any]] | None
|
364
406
|
) -> bool:
|
365
407
|
"""Update thermostat sensors and dispatch the events."""
|
408
|
+
_LOGGER.info("Received thermostat sensors")
|
366
409
|
if sensors:
|
367
410
|
await asyncio.gather(
|
368
411
|
*(
|
@@ -377,11 +420,12 @@ class EcoMAX(PhysicalDevice):
|
|
377
420
|
|
378
421
|
return False
|
379
422
|
|
380
|
-
@event_listener
|
423
|
+
@event_listener
|
381
424
|
async def on_event_schedules(
|
382
425
|
self, schedules: list[tuple[int, list[list[bool]]]]
|
383
426
|
) -> dict[str, Schedule]:
|
384
427
|
"""Update schedules."""
|
428
|
+
_LOGGER.info("Received device schedules")
|
385
429
|
return {
|
386
430
|
SCHEDULES[index]: Schedule(
|
387
431
|
name=SCHEDULES[index],
|
@@ -397,7 +441,7 @@ class EcoMAX(PhysicalDevice):
|
|
397
441
|
for index, schedule in schedules
|
398
442
|
}
|
399
443
|
|
400
|
-
@event_listener(
|
444
|
+
@event_listener(filter=on_change)
|
401
445
|
async def on_event_state(self, state: DeviceState) -> None:
|
402
446
|
"""Update the ecoMAX control parameter."""
|
403
447
|
await self.dispatch(
|
pyplumio/devices/mixer.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Coroutine, Generator
|
6
|
+
from collections.abc import Coroutine, Generator
|
7
7
|
import logging
|
8
8
|
from typing import Any
|
9
9
|
|
@@ -16,8 +16,6 @@ from pyplumio.parameters.mixer import (
|
|
16
16
|
MixerSwitchDescription,
|
17
17
|
get_mixer_parameter_types,
|
18
18
|
)
|
19
|
-
from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
|
20
|
-
from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
|
21
19
|
from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
|
22
20
|
|
23
21
|
_LOGGER = logging.getLogger(__name__)
|
@@ -28,19 +26,21 @@ class Mixer(VirtualDevice):
|
|
28
26
|
|
29
27
|
__slots__ = ()
|
30
28
|
|
31
|
-
@event_listener
|
29
|
+
@event_listener
|
32
30
|
async def on_event_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
|
33
31
|
"""Update mixer sensors and dispatch the events."""
|
32
|
+
_LOGGER.info("Received mixer %i sensors", self.index)
|
34
33
|
await asyncio.gather(
|
35
34
|
*(self.dispatch(name, value) for name, value in sensors.items())
|
36
35
|
)
|
37
36
|
return True
|
38
37
|
|
39
|
-
@event_listener
|
38
|
+
@event_listener
|
40
39
|
async def on_event_mixer_parameters(
|
41
|
-
self, parameters:
|
40
|
+
self, parameters: list[tuple[int, ParameterValues]]
|
42
41
|
) -> bool:
|
43
42
|
"""Update mixer parameters and dispatch the events."""
|
43
|
+
_LOGGER.info("Received mixer %i parameters", self.index)
|
44
44
|
product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
|
45
45
|
|
46
46
|
def _mixer_parameter_events() -> Generator[Coroutine, Any, None]:
|
pyplumio/devices/thermostat.py
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Coroutine, Generator
|
6
|
+
from collections.abc import Coroutine, Generator
|
7
|
+
import logging
|
7
8
|
from typing import Any
|
8
9
|
|
9
10
|
from pyplumio.devices import VirtualDevice
|
@@ -15,8 +16,8 @@ from pyplumio.parameters.thermostat import (
|
|
15
16
|
ThermostatSwitchDescription,
|
16
17
|
get_thermostat_parameter_types,
|
17
18
|
)
|
18
|
-
|
19
|
-
|
19
|
+
|
20
|
+
_LOGGER = logging.getLogger()
|
20
21
|
|
21
22
|
|
22
23
|
class Thermostat(VirtualDevice):
|
@@ -24,19 +25,21 @@ class Thermostat(VirtualDevice):
|
|
24
25
|
|
25
26
|
__slots__ = ()
|
26
27
|
|
27
|
-
@event_listener
|
28
|
+
@event_listener
|
28
29
|
async def on_event_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
|
29
30
|
"""Update thermostat sensors and dispatch the events."""
|
31
|
+
_LOGGER.info("Received thermostat %i sensors", self.index)
|
30
32
|
await asyncio.gather(
|
31
33
|
*(self.dispatch(name, value) for name, value in sensors.items())
|
32
34
|
)
|
33
35
|
return True
|
34
36
|
|
35
|
-
@event_listener
|
37
|
+
@event_listener
|
36
38
|
async def on_event_thermostat_parameters(
|
37
|
-
self, parameters:
|
39
|
+
self, parameters: list[tuple[int, ParameterValues]]
|
38
40
|
) -> bool:
|
39
41
|
"""Update thermostat parameters and dispatch the events."""
|
42
|
+
_LOGGER.info("Received thermostat %i parameters", self.index)
|
40
43
|
|
41
44
|
def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
|
42
45
|
"""Get dispatch calls for thermostat parameter events."""
|
pyplumio/filters.py
CHANGED
@@ -4,7 +4,10 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
from collections.abc import Callable
|
7
|
+
from contextlib import suppress
|
7
8
|
from copy import copy
|
9
|
+
from decimal import Decimal
|
10
|
+
import logging
|
8
11
|
import math
|
9
12
|
import time
|
10
13
|
from typing import (
|
@@ -22,6 +25,16 @@ from typing_extensions import TypeAlias
|
|
22
25
|
from pyplumio.helpers.event_manager import Callback
|
23
26
|
from pyplumio.parameters import Parameter
|
24
27
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
numpy_installed = False
|
31
|
+
with suppress(ImportError):
|
32
|
+
import numpy as np
|
33
|
+
|
34
|
+
_LOGGER.info("Using numpy for improved float precision")
|
35
|
+
numpy_installed = True
|
36
|
+
|
37
|
+
|
25
38
|
UNDEFINED: Final = "undefined"
|
26
39
|
TOLERANCE: Final = 0.1
|
27
40
|
|
@@ -129,44 +142,50 @@ class _Aggregate(Filter):
|
|
129
142
|
"""Represents an aggregate filter.
|
130
143
|
|
131
144
|
Calls a callback with a sum of values collected over a specified
|
132
|
-
time period.
|
145
|
+
time period or when sample size limit reached.
|
133
146
|
"""
|
134
147
|
|
135
|
-
__slots__ = ("
|
148
|
+
__slots__ = ("_values", "_sample_size", "_timeout", "_last_call_time")
|
136
149
|
|
137
|
-
|
138
|
-
|
150
|
+
_values: list[float | int | Decimal]
|
151
|
+
_sample_size: int
|
139
152
|
_timeout: float
|
153
|
+
_last_call_time: float
|
140
154
|
|
141
|
-
def __init__(self, callback: Callback, seconds: float) -> None:
|
155
|
+
def __init__(self, callback: Callback, seconds: float, sample_size: int) -> None:
|
142
156
|
"""Initialize a new aggregate filter."""
|
143
157
|
super().__init__(callback)
|
144
|
-
self.
|
158
|
+
self._last_call_time = time.monotonic()
|
145
159
|
self._timeout = seconds
|
146
|
-
self.
|
160
|
+
self._sample_size = sample_size
|
161
|
+
self._values = []
|
147
162
|
|
148
163
|
async def __call__(self, new_value: Any) -> Any:
|
149
164
|
"""Set a new value for the callback."""
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
165
|
+
if not isinstance(new_value, (float, int, Decimal)):
|
166
|
+
raise TypeError(
|
167
|
+
"Aggregate filter can only be used with numeric values, got "
|
168
|
+
f"{type(new_value).__name__}: {new_value}"
|
169
|
+
)
|
170
|
+
|
171
|
+
current_time = time.monotonic()
|
172
|
+
self._values.append(new_value)
|
173
|
+
time_since_call = current_time - self._last_call_time
|
174
|
+
if time_since_call >= self._timeout or len(self._values) >= self._sample_size:
|
175
|
+
result = await self._callback(
|
176
|
+
np.sum(self._values) if numpy_installed else sum(self._values)
|
177
|
+
)
|
178
|
+
self._last_call_time = current_time
|
179
|
+
self._values = []
|
162
180
|
return result
|
163
181
|
|
164
182
|
|
165
|
-
def aggregate(callback: Callback, seconds: float) -> _Aggregate:
|
183
|
+
def aggregate(callback: Callback, seconds: float, sample_size: int) -> _Aggregate:
|
166
184
|
"""Create a new aggregate filter.
|
167
185
|
|
168
186
|
A callback function will be called with a sum of values collected
|
169
|
-
over a specified time period
|
187
|
+
over a specified time period or when sample size limit reached.
|
188
|
+
Can only be used with numeric values.
|
170
189
|
|
171
190
|
:param callback: A callback function to be awaited once filter
|
172
191
|
conditions are fulfilled
|
@@ -174,10 +193,13 @@ def aggregate(callback: Callback, seconds: float) -> _Aggregate:
|
|
174
193
|
:param seconds: A callback will be awaited with a sum of values
|
175
194
|
aggregated over this amount of seconds.
|
176
195
|
:type seconds: float
|
196
|
+
:param sample_size: The maximum number of values to aggregate
|
197
|
+
before calling the callback
|
198
|
+
:type sample_size: int
|
177
199
|
:return: An instance of callable filter
|
178
200
|
:rtype: _Aggregate
|
179
201
|
"""
|
180
|
-
return _Aggregate(callback, seconds)
|
202
|
+
return _Aggregate(callback, seconds, sample_size)
|
181
203
|
|
182
204
|
|
183
205
|
class _Clamp(Filter):
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Contains a simple async cache for caching results of async functions."""
|
2
|
+
|
3
|
+
from collections.abc import Awaitable
|
4
|
+
from functools import wraps
|
5
|
+
from typing import Any, Callable, TypeVar, cast
|
6
|
+
|
7
|
+
from typing_extensions import ParamSpec
|
8
|
+
|
9
|
+
T = TypeVar("T")
|
10
|
+
P = ParamSpec("P")
|
11
|
+
|
12
|
+
|
13
|
+
class AsyncCache:
|
14
|
+
"""A simple cache for asynchronous functions."""
|
15
|
+
|
16
|
+
__slots__ = ("cache",)
|
17
|
+
|
18
|
+
cache: dict[str, Any]
|
19
|
+
|
20
|
+
def __init__(self) -> None:
|
21
|
+
"""Initialize the cache."""
|
22
|
+
self.cache = {}
|
23
|
+
|
24
|
+
async def get(self, key: str, coro: Callable[..., Awaitable[Any]]) -> Any:
|
25
|
+
"""Get a value from the cache or compute and store it."""
|
26
|
+
if key not in self.cache:
|
27
|
+
self.cache[key] = await coro()
|
28
|
+
|
29
|
+
return self.cache[key]
|
30
|
+
|
31
|
+
|
32
|
+
# Create a global cache instance
|
33
|
+
async_cache = AsyncCache()
|
34
|
+
|
35
|
+
|
36
|
+
def acache(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
37
|
+
"""Cache the result of an async function."""
|
38
|
+
|
39
|
+
@wraps(func)
|
40
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
41
|
+
func_name = f"{func.__module__}.{func.__qualname__}"
|
42
|
+
key = f"{func_name}:{args}:{kwargs}"
|
43
|
+
return cast(T, await async_cache.get(key, lambda: func(*args, **kwargs)))
|
44
|
+
|
45
|
+
return wrapper
|
46
|
+
|
47
|
+
|
48
|
+
__all__ = ["acache", "async_cache"]
|
@@ -17,7 +17,17 @@ _CallableT: TypeAlias = Callable[..., Any]
|
|
17
17
|
_CallbackT = TypeVar("_CallbackT", bound=Callback)
|
18
18
|
|
19
19
|
|
20
|
-
|
20
|
+
@overload
|
21
|
+
def event_listener(name: _CallableT, filter: None = None) -> Callback: ...
|
22
|
+
|
23
|
+
|
24
|
+
@overload
|
25
|
+
def event_listener(
|
26
|
+
name: str | None = None, filter: _CallableT | None = None
|
27
|
+
) -> _CallableT: ...
|
28
|
+
|
29
|
+
|
30
|
+
def event_listener(name: Any = None, filter: _CallableT | None = None) -> Any:
|
21
31
|
"""Mark a function as an event listener.
|
22
32
|
|
23
33
|
This decorator attaches metadata to the function, identifying it
|
@@ -26,11 +36,19 @@ def event_listener(event: str, filter: _CallableT | None = None) -> _CallableT:
|
|
26
36
|
|
27
37
|
def decorator(func: _CallbackT) -> _CallbackT:
|
28
38
|
# Attach metadata to the function to mark it as a listener.
|
39
|
+
event = (
|
40
|
+
name
|
41
|
+
if isinstance(name, str)
|
42
|
+
else func.__qualname__.split("on_event_", 1)[1]
|
43
|
+
)
|
29
44
|
setattr(func, "_on_event", event)
|
30
45
|
setattr(func, "_on_event_filter", filter)
|
31
46
|
return func
|
32
47
|
|
33
|
-
|
48
|
+
if callable(name):
|
49
|
+
return decorator(name)
|
50
|
+
else:
|
51
|
+
return decorator
|
34
52
|
|
35
53
|
|
36
54
|
T = TypeVar("T")
|
pyplumio/helpers/timeout.py
CHANGED
@@ -5,19 +5,20 @@ from __future__ import annotations
|
|
5
5
|
import asyncio
|
6
6
|
from collections.abc import Awaitable, Callable
|
7
7
|
from functools import wraps
|
8
|
-
from typing import
|
8
|
+
from typing import TypeVar
|
9
9
|
|
10
|
-
from typing_extensions import ParamSpec
|
10
|
+
from typing_extensions import ParamSpec
|
11
11
|
|
12
12
|
T = TypeVar("T")
|
13
13
|
P = ParamSpec("P")
|
14
|
-
_CallableT: TypeAlias = Callable[..., Any]
|
15
14
|
|
16
15
|
|
17
|
-
def timeout(
|
16
|
+
def timeout(
|
17
|
+
seconds: float,
|
18
|
+
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
18
19
|
"""Decorate a timeout for the awaitable."""
|
19
20
|
|
20
|
-
def decorator(func: Callable[P, Awaitable[T]]) ->
|
21
|
+
def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
21
22
|
@wraps(func)
|
22
23
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
23
24
|
return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
|
pyplumio/parameters/__init__.py
CHANGED
@@ -196,7 +196,7 @@ class Parameter(ABC):
|
|
196
196
|
self, value: int, retries: int = 5, timeout: float = 5.0
|
197
197
|
) -> bool:
|
198
198
|
"""Attempt to update a parameter value on the remote device."""
|
199
|
-
_LOGGER.
|
199
|
+
_LOGGER.info(
|
200
200
|
"Attempting to update '%s' parameter to %d", self.description.name, value
|
201
201
|
)
|
202
202
|
if value == self.values.value:
|
@@ -509,8 +509,8 @@ def patch_parameter_types(
|
|
509
509
|
"""Patch the parameter types based on the provided overrides.
|
510
510
|
|
511
511
|
Note:
|
512
|
-
The `# type: ignore[assignment]` comment is used to suppress a
|
513
|
-
error caused by mypy bug. For more details, see:
|
512
|
+
The `# type: ignore[assignment]` comment is used to suppress a
|
513
|
+
type-checking error caused by mypy bug. For more details, see:
|
514
514
|
https://github.com/python/mypy/issues/13596
|
515
515
|
|
516
516
|
"""
|
@@ -522,6 +522,12 @@ def patch_parameter_types(
|
|
522
522
|
}
|
523
523
|
for index, description in enumerate(parameter_types):
|
524
524
|
if description.name in replacements:
|
525
|
+
_LOGGER.info(
|
526
|
+
"Replacing parameter description for '%s' with '%s' (%s)",
|
527
|
+
description.name,
|
528
|
+
replacements[description.name],
|
529
|
+
product_info.model,
|
530
|
+
)
|
525
531
|
parameter_types[index] = replacements[description.name] # type: ignore[assignment]
|
526
532
|
|
527
533
|
return parameter_types
|
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 (
|
@@ -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,18 +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
|
237
244
|
|
238
|
-
|
245
|
+
async with self._entry_lock:
|
246
|
+
return await _setup_device_entry(device_type)
|
239
247
|
|
240
248
|
|
241
249
|
__all__ = ["Protocol", "DummyProtocol", "AsyncProtocol"]
|
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
|
|
@@ -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
|
|
pyplumio/structures/fan_power.py
CHANGED
@@ -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
|
|
@@ -6,7 +6,7 @@ from contextlib import suppress
|
|
6
6
|
from typing import Any, Final
|
7
7
|
|
8
8
|
from pyplumio.const import FrameType
|
9
|
-
from pyplumio.
|
9
|
+
from pyplumio.data_types import UnsignedShort
|
10
10
|
from pyplumio.structures import StructureDecoder
|
11
11
|
from pyplumio.utils import ensure_dict
|
12
12
|
|
@@ -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
|
|
@@ -7,7 +7,7 @@ import math
|
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
9
|
from pyplumio.const import BYTE_UNDEFINED, LambdaState
|
10
|
-
from pyplumio.
|
10
|
+
from pyplumio.data_types import UnsignedShort
|
11
11
|
from pyplumio.structures import StructureDecoder
|
12
12
|
from pyplumio.utils import ensure_dict
|
13
13
|
|
@@ -7,7 +7,7 @@ import math
|
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
9
|
from pyplumio.const import ATTR_CURRENT_TEMP, ATTR_TARGET_TEMP
|
10
|
-
from pyplumio.
|
10
|
+
from pyplumio.data_types import Float
|
11
11
|
from pyplumio.structures import StructureDecoder
|
12
12
|
from pyplumio.utils import ensure_dict
|
13
13
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
6
6
|
from typing import Any, Final
|
7
7
|
|
8
8
|
from pyplumio.const import EncryptionType
|
9
|
-
from pyplumio.
|
9
|
+
from pyplumio.data_types import IPv4, VarString
|
10
10
|
from pyplumio.structures import Structure
|
11
11
|
from pyplumio.utils import ensure_dict
|
12
12
|
|
pyplumio/structures/outputs.py
CHANGED
@@ -9,7 +9,7 @@ import struct
|
|
9
9
|
from typing import Any, Final
|
10
10
|
|
11
11
|
from pyplumio.const import ProductType
|
12
|
-
from pyplumio.
|
12
|
+
from pyplumio.data_types import UnsignedShort, VarBytes, VarString
|
13
13
|
from pyplumio.helpers.uid import unpack_uid
|
14
14
|
from pyplumio.structures import StructureDecoder
|
15
15
|
from pyplumio.utils import ensure_dict
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from typing import Any, Final
|
6
6
|
|
7
|
-
from pyplumio.
|
7
|
+
from pyplumio.data_types import BitArray, DataType
|
8
8
|
from pyplumio.structures import StructureDecoder
|
9
9
|
from pyplumio.structures.frame_versions import FrameVersionsStructure
|
10
10
|
from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from typing import Any, Final
|
6
6
|
|
7
|
-
from pyplumio.
|
7
|
+
from pyplumio.data_types import DATA_TYPES, DataType, UnsignedShort
|
8
8
|
from pyplumio.structures import StructureDecoder
|
9
9
|
from pyplumio.utils import ensure_dict
|
10
10
|
|
@@ -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
|
|
@@ -52,13 +52,11 @@ class ThermostatParametersStructure(StructureDecoder):
|
|
52
52
|
self, message: bytearray, offset: int = 0, data: dict[str, Any] | None = None
|
53
53
|
) -> tuple[dict[str, Any], int]:
|
54
54
|
"""Decode bytes and return message data and offset."""
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
offset,
|
61
|
-
)
|
55
|
+
device = self.frame.handler
|
56
|
+
if not device or not (
|
57
|
+
thermostats := device.get_nowait(ATTR_THERMOSTATS_AVAILABLE, None)
|
58
|
+
):
|
59
|
+
return ensure_dict(data, {ATTR_THERMOSTAT_PARAMETERS: None}), offset
|
62
60
|
|
63
61
|
start = message[offset + 1]
|
64
62
|
end = message[offset + 2]
|
@@ -13,7 +13,7 @@ from pyplumio.const import (
|
|
13
13
|
ATTR_TARGET_TEMP,
|
14
14
|
BYTE_UNDEFINED,
|
15
15
|
)
|
16
|
-
from pyplumio.
|
16
|
+
from pyplumio.data_types import Float
|
17
17
|
from pyplumio.structures import StructureDecoder
|
18
18
|
from pyplumio.utils import ensure_dict
|
19
19
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.44
|
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
|
@@ -29,7 +29,9 @@ Requires-Dist: typing-extensions==4.13.2
|
|
29
29
|
Provides-Extra: test
|
30
30
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
31
|
Requires-Dist: coverage==7.8.0; extra == "test"
|
32
|
+
Requires-Dist: freezegun==1.5.1; extra == "test"
|
32
33
|
Requires-Dist: mypy==1.15.0; extra == "test"
|
34
|
+
Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
33
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
34
36
|
Requires-Dist: pytest==8.3.5; extra == "test"
|
35
37
|
Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
|
@@ -0,0 +1,64 @@
|
|
1
|
+
pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
|
2
|
+
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
+
pyplumio/_version.py,sha256=jmLwpXAvOwShA-gbD-jNugBDxB5dkqo8l6U4A1WR2nw,513
|
4
|
+
pyplumio/connection.py,sha256=9MCPb8W62uqCrzd1YCROcn9cCjRY8E65934FnJDF5Js,5902
|
5
|
+
pyplumio/const.py,sha256=FxF97bl_GunYuB8Wo72zCzHtznRCM64ygC2qfaR3UyA,5684
|
6
|
+
pyplumio/data_types.py,sha256=r-QOIZiIpBFo4kRongyu8n0BHTaEU6wWMTmNkWBNjq8,9223
|
7
|
+
pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
|
8
|
+
pyplumio/filters.py,sha256=1AIEesbIBOceLdz--7SwDb8IPn8aLNk4gd1F6EJCZ-s,13667
|
9
|
+
pyplumio/protocol.py,sha256=UnrXDouo4dDi7hqwJYHoEAvCoYJs3PgP1DFBBwRqBrw,8427
|
10
|
+
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
pyplumio/stream.py,sha256=iCB-XlNXRRE0p7nstb1AkXwVDwcCsgym_ggB_IDiJc8,4926
|
12
|
+
pyplumio/utils.py,sha256=D6_SJzYkFjXoUrlNPt_mIQAP8hjMU05RsTqlAFphj3Y,1205
|
13
|
+
pyplumio/devices/__init__.py,sha256=dqEB8swvH5ZnY7KKyTJs9f3GR37I_0PrsY9Uet-663w,7926
|
14
|
+
pyplumio/devices/ecomax.py,sha256=GgugaaaVHHoCrl9zGH2B5WBlMJF_CVTkRwMIe_xJuYI,16114
|
15
|
+
pyplumio/devices/ecoster.py,sha256=X46ky5XT8jHMFq9sBW0ve8ZI_tjItQDMt4moXsW-ogY,307
|
16
|
+
pyplumio/devices/mixer.py,sha256=cuqhWcIeGgHNIVqmRm-NyvaRb-ltGY5YZXrQN81xKP0,2744
|
17
|
+
pyplumio/devices/thermostat.py,sha256=Wylm0xgheS6E_y-w0E8JEKax6Gckfw7qZu9fgv8iSMc,2266
|
18
|
+
pyplumio/frames/__init__.py,sha256=5lw19oFlN89ZvO8KGwnkwERULQNYiP-hhZKk65LsjYY,7862
|
19
|
+
pyplumio/frames/messages.py,sha256=ImQGWFFTa2eaXfytQmFZKC-IxyPRkxD8qp0bEm16-ws,3628
|
20
|
+
pyplumio/frames/requests.py,sha256=jr-_XSSCCDDTbAmrw95CKyWa5nb7JNeGzZ2jDXIxlAo,7348
|
21
|
+
pyplumio/frames/responses.py,sha256=M6Ky4gg2AoShmRXX0x6nftajxrvmQLKPVRWbwyhvI0E,6663
|
22
|
+
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
23
|
+
pyplumio/helpers/async_cache.py,sha256=TlxpL4P3IZEobS4KyP86cEtqZghKtPWmVj57n5FpvTQ,1259
|
24
|
+
pyplumio/helpers/event_manager.py,sha256=aKNlhsPNTy3eOSfWVb9TJxtIsN9GAQv9XxhOi_BOhlM,8097
|
25
|
+
pyplumio/helpers/factory.py,sha256=AUPnTLDAPL8uq-fxn7ndMs_mOrocEo6keQjDk4Ig1ko,1156
|
26
|
+
pyplumio/helpers/schedule.py,sha256=4XdijJBnhsfsj3l2hiKsrLA_a6oxTzg67ZnjG2IJlB0,5301
|
27
|
+
pyplumio/helpers/task_manager.py,sha256=N71F6Ag1HHxdf5zJeCMcEziFdH9lmJKtMPoRGjJ-E40,1209
|
28
|
+
pyplumio/helpers/timeout.py,sha256=ZTWMhgk5lOUNsR-qqNxUOif5rDHH1eOMAvRTngb1QH4,745
|
29
|
+
pyplumio/helpers/uid.py,sha256=HeM5Zmd0qfNmVya6RKE-bBYzdxG-pAiViOZRHqw33VU,1011
|
30
|
+
pyplumio/parameters/__init__.py,sha256=kEDaPqJLRemc6puIGThmjfAbdt86oZnXIUE34scXG5Q,16603
|
31
|
+
pyplumio/parameters/ecomax.py,sha256=MfjiXYW_MoEPMPdmeeZi2Wb8tNhZKTHXYxSBCQNYf9g,27606
|
32
|
+
pyplumio/parameters/mixer.py,sha256=RjhfUU_62STyNV0Ud9A4G4FEvVwo02qGVl8h1QvqQXI,6646
|
33
|
+
pyplumio/parameters/thermostat.py,sha256=-DK2Mb78CGrKmdhwAD0M3GiGJatczPnl1e2gVeT19tI,5070
|
34
|
+
pyplumio/structures/__init__.py,sha256=emZVH5OFgdTUPbEJoznMKitmK0nlPm0I4SmF86It1Do,1345
|
35
|
+
pyplumio/structures/alerts.py,sha256=fglFcxHoZ3hZJscPHmbJJqTr2mH8YXRW0T18L4Dirds,3692
|
36
|
+
pyplumio/structures/boiler_load.py,sha256=e-6itp9L6iJeeOyhSTiOclHLuYmqG7KkcepsHwJSQSI,894
|
37
|
+
pyplumio/structures/boiler_power.py,sha256=7CdOk-pYLEpy06oRBAeichvq8o-a2RcesB0tzo9ccBs,951
|
38
|
+
pyplumio/structures/ecomax_parameters.py,sha256=E_s5bO0RqX8p1rM5DtYAsEXcHqS8P6Tg4AGm21cxsnM,1663
|
39
|
+
pyplumio/structures/fan_power.py,sha256=l9mDB_Ugnn1gKJFh9fppwzoi0i1_3eBvHAD6uPAdiDI,915
|
40
|
+
pyplumio/structures/frame_versions.py,sha256=n-L93poxY3i_l3oMWg0PzRXGrkY9cgv-Eyuf9XObaR4,1602
|
41
|
+
pyplumio/structures/fuel_consumption.py,sha256=Cf3Z14gEZnkVEt-OAXNka3-T8fKIMHQaVSeQdQYXnPg,1034
|
42
|
+
pyplumio/structures/fuel_level.py,sha256=-zUKApVJaZZzc1q52vqO3K2Mya43c6vRgw45d2xgy5Q,1123
|
43
|
+
pyplumio/structures/lambda_sensor.py,sha256=0ZNEhzvNVtZt9CY0ZnZ7-N3fdWnuuexcrkztKq-lBSw,1708
|
44
|
+
pyplumio/structures/mixer_parameters.py,sha256=JMSySqI7TUGKdFtDp1P5DJm5EAbijMSz-orRrAe1KlQ,2041
|
45
|
+
pyplumio/structures/mixer_sensors.py,sha256=ChgLhC3p4fyoPy1EKe0BQTvXOPZEISbcK2HyamrNaN8,2450
|
46
|
+
pyplumio/structures/modules.py,sha256=LviFz3_pPvSQ5i_Mr2S9o5miVad8O4qn48CR_z7yG24,2791
|
47
|
+
pyplumio/structures/network_info.py,sha256=HYhROSMbVxqYxsOa7aF3xetQXEs0xGvhzH-4OHETZVQ,4327
|
48
|
+
pyplumio/structures/output_flags.py,sha256=upVIgAH2JNncHFXvjE-t6oTFF-JNwwZbyGjfrcKWtz0,1508
|
49
|
+
pyplumio/structures/outputs.py,sha256=3NP5lArzQiihRC4QzBuWAHL9hhjvGxNkKmeoYZnDD-0,2291
|
50
|
+
pyplumio/structures/pending_alerts.py,sha256=b1uMmDHTGv8eE0h1vGBrKsPxlwBmUad7HgChnDDLK_g,801
|
51
|
+
pyplumio/structures/product_info.py,sha256=yXHFQv6LcA7kj8ErAY-fK4z33EIgDnvVDNe3Dccg_Bo,2472
|
52
|
+
pyplumio/structures/program_version.py,sha256=qHmmPComCOa-dgq7cFAucEGuRS-jWYwWi40VCiPS7cc,2621
|
53
|
+
pyplumio/structures/regulator_data.py,sha256=SYKI1YPC3mDAth-SpYejttbD0IzBfobjgC-uy_uUKnw,2333
|
54
|
+
pyplumio/structures/regulator_data_schema.py,sha256=0SapbZCGzqAHmHC7dwhufszJ9FNo_ZO_XMrFGNiUe-w,1547
|
55
|
+
pyplumio/structures/schedules.py,sha256=JWMyi-D_a2M8k17Zis7-o9L3Zn-Lvzdh1ewXDQeoaYo,7092
|
56
|
+
pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8pLI,1325
|
57
|
+
pyplumio/structures/temperatures.py,sha256=2VD3P_vwp9PEBkOn2-WhifOR8w-UYNq35aAxle0z2Vg,2831
|
58
|
+
pyplumio/structures/thermostat_parameters.py,sha256=st3x3HkjQm3hqBrn_fpvPDQu8fuc-Sx33ONB19ViQak,3007
|
59
|
+
pyplumio/structures/thermostat_sensors.py,sha256=rO9jTZWGQpThtJqVdbbv8sYMYHxJi4MfwZQza69L2zw,3399
|
60
|
+
pyplumio-0.5.44.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
61
|
+
pyplumio-0.5.44.dist-info/METADATA,sha256=ShYqD3uUOfz0g4IhgAT1fA15CjI9RtXViXYY238Nsuo,5611
|
62
|
+
pyplumio-0.5.44.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
63
|
+
pyplumio-0.5.44.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
64
|
+
pyplumio-0.5.44.dist-info/RECORD,,
|
pyplumio-0.5.43.dist-info/RECORD
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
|
2
|
-
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
-
pyplumio/_version.py,sha256=AtaNtvbmZhmRRCLmwQp9AiFD_qy1xi0FmP9tZWoRRsg,513
|
4
|
-
pyplumio/connection.py,sha256=9MCPb8W62uqCrzd1YCROcn9cCjRY8E65934FnJDF5Js,5902
|
5
|
-
pyplumio/const.py,sha256=efsoFXbja8oFhn0fiATnHsNKhV26z0XdWHn84MHc1pE,5688
|
6
|
-
pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
|
7
|
-
pyplumio/filters.py,sha256=Z_U3V7VEBkCwXWcAy9zmauoZtQj7BSCEODzQhT9K2tc,12784
|
8
|
-
pyplumio/protocol.py,sha256=oyZZKVazLTlS1sMKQA4zThp39-HqIGXW7BUGctHznN4,8146
|
9
|
-
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
pyplumio/stream.py,sha256=iCB-XlNXRRE0p7nstb1AkXwVDwcCsgym_ggB_IDiJc8,4926
|
11
|
-
pyplumio/utils.py,sha256=D6_SJzYkFjXoUrlNPt_mIQAP8hjMU05RsTqlAFphj3Y,1205
|
12
|
-
pyplumio/devices/__init__.py,sha256=70MJSd43ipd6V0E7az0Ba_5JKTX3ICE3Ct-GDX8GTzU,8370
|
13
|
-
pyplumio/devices/ecomax.py,sha256=_lAMMZw9hW1KEKXREgaOZTIsvBPss_fDGjB44GZNXUM,15005
|
14
|
-
pyplumio/devices/ecoster.py,sha256=X46ky5XT8jHMFq9sBW0ve8ZI_tjItQDMt4moXsW-ogY,307
|
15
|
-
pyplumio/devices/mixer.py,sha256=YLe_3XCjjX0PHrICPT-dR7uelTTFofHlqC8twtg8Wyg,2810
|
16
|
-
pyplumio/devices/thermostat.py,sha256=OYWYBl-8R7ooTgAQSHkhwHH74Rjjh8IJJZNyi93omgg,2306
|
17
|
-
pyplumio/frames/__init__.py,sha256=5lw19oFlN89ZvO8KGwnkwERULQNYiP-hhZKk65LsjYY,7862
|
18
|
-
pyplumio/frames/messages.py,sha256=ImQGWFFTa2eaXfytQmFZKC-IxyPRkxD8qp0bEm16-ws,3628
|
19
|
-
pyplumio/frames/requests.py,sha256=jr-_XSSCCDDTbAmrw95CKyWa5nb7JNeGzZ2jDXIxlAo,7348
|
20
|
-
pyplumio/frames/responses.py,sha256=M6Ky4gg2AoShmRXX0x6nftajxrvmQLKPVRWbwyhvI0E,6663
|
21
|
-
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
22
|
-
pyplumio/helpers/data_types.py,sha256=r-QOIZiIpBFo4kRongyu8n0BHTaEU6wWMTmNkWBNjq8,9223
|
23
|
-
pyplumio/helpers/event_manager.py,sha256=J-y5ErPvR2ek3rhYrASElwgtCaEM6wuELJQqH74VIWY,7686
|
24
|
-
pyplumio/helpers/factory.py,sha256=AUPnTLDAPL8uq-fxn7ndMs_mOrocEo6keQjDk4Ig1ko,1156
|
25
|
-
pyplumio/helpers/schedule.py,sha256=4XdijJBnhsfsj3l2hiKsrLA_a6oxTzg67ZnjG2IJlB0,5301
|
26
|
-
pyplumio/helpers/task_manager.py,sha256=N71F6Ag1HHxdf5zJeCMcEziFdH9lmJKtMPoRGjJ-E40,1209
|
27
|
-
pyplumio/helpers/timeout.py,sha256=nbro-58ncTCI___kMtE221_UVwNXtqt-oY4Dc5sx_Dg,728
|
28
|
-
pyplumio/helpers/uid.py,sha256=HeM5Zmd0qfNmVya6RKE-bBYzdxG-pAiViOZRHqw33VU,1011
|
29
|
-
pyplumio/parameters/__init__.py,sha256=1K4TzQ3qhTX7Slixap_PW4N38d1whgXm13mNwjRuw-s,16371
|
30
|
-
pyplumio/parameters/ecomax.py,sha256=MfjiXYW_MoEPMPdmeeZi2Wb8tNhZKTHXYxSBCQNYf9g,27606
|
31
|
-
pyplumio/parameters/mixer.py,sha256=RjhfUU_62STyNV0Ud9A4G4FEvVwo02qGVl8h1QvqQXI,6646
|
32
|
-
pyplumio/parameters/thermostat.py,sha256=-DK2Mb78CGrKmdhwAD0M3GiGJatczPnl1e2gVeT19tI,5070
|
33
|
-
pyplumio/structures/__init__.py,sha256=emZVH5OFgdTUPbEJoznMKitmK0nlPm0I4SmF86It1Do,1345
|
34
|
-
pyplumio/structures/alerts.py,sha256=teoUlbhvpnPLntPXCRmNO7YZa0RxvPBD724mqxkQD28,3700
|
35
|
-
pyplumio/structures/boiler_load.py,sha256=e-6itp9L6iJeeOyhSTiOclHLuYmqG7KkcepsHwJSQSI,894
|
36
|
-
pyplumio/structures/boiler_power.py,sha256=Nxc8DiOQRD6dr3fo-xxiu171t9ablYJIRL8RQe0SEzo,959
|
37
|
-
pyplumio/structures/ecomax_parameters.py,sha256=E_s5bO0RqX8p1rM5DtYAsEXcHqS8P6Tg4AGm21cxsnM,1663
|
38
|
-
pyplumio/structures/fan_power.py,sha256=MwiaU8ht8BLSaGLw5dQvVleDcHCtyrvO1DT9iX0oxlg,923
|
39
|
-
pyplumio/structures/frame_versions.py,sha256=MS5bLW1y3SRq6Swjd1mkzy9xJkBXYQq313XAY1RnMjc,1610
|
40
|
-
pyplumio/structures/fuel_consumption.py,sha256=Ki_i_T4fq--6F8d4spXuY13JEYZllGwcU5U589mwuKo,1042
|
41
|
-
pyplumio/structures/fuel_level.py,sha256=-zUKApVJaZZzc1q52vqO3K2Mya43c6vRgw45d2xgy5Q,1123
|
42
|
-
pyplumio/structures/lambda_sensor.py,sha256=ySSSsN5BHvvvZgJX2rh7K_cdfW0QI_uGH-l2XqWJmmQ,1716
|
43
|
-
pyplumio/structures/mixer_parameters.py,sha256=JMSySqI7TUGKdFtDp1P5DJm5EAbijMSz-orRrAe1KlQ,2041
|
44
|
-
pyplumio/structures/mixer_sensors.py,sha256=B7jBL4KPE3lCh9TX_mLcsOCdabKbgudu4FJSWqud1QE,2458
|
45
|
-
pyplumio/structures/modules.py,sha256=LviFz3_pPvSQ5i_Mr2S9o5miVad8O4qn48CR_z7yG24,2791
|
46
|
-
pyplumio/structures/network_info.py,sha256=EpqCaHY7V3gVzZ04STy08vv7RUdtI9tUAhWWlqBnMMQ,4335
|
47
|
-
pyplumio/structures/output_flags.py,sha256=sqZm-jJC9MuJM1qezMh59wan7MUpqEXgahdHc0nzmYY,1516
|
48
|
-
pyplumio/structures/outputs.py,sha256=9S7Cvc6Jjy1kLgV4d5SNY2suWrUo4gwbp7PDoAdGhkA,2299
|
49
|
-
pyplumio/structures/pending_alerts.py,sha256=b1uMmDHTGv8eE0h1vGBrKsPxlwBmUad7HgChnDDLK_g,801
|
50
|
-
pyplumio/structures/product_info.py,sha256=IwEfXQ6LGyhoh3ox_i8dRurT0gKJfQdhYln76Q268TI,2480
|
51
|
-
pyplumio/structures/program_version.py,sha256=qHmmPComCOa-dgq7cFAucEGuRS-jWYwWi40VCiPS7cc,2621
|
52
|
-
pyplumio/structures/regulator_data.py,sha256=H5vg70EV_OkJ6Np3qkQ-ekXYdzHH5S2YGv9SGfpXRhY,2341
|
53
|
-
pyplumio/structures/regulator_data_schema.py,sha256=dY3YeaTfwbhz3jyvYtSLJBxyASDqPZsEeGIHN_Rbew0,1555
|
54
|
-
pyplumio/structures/schedules.py,sha256=JWMyi-D_a2M8k17Zis7-o9L3Zn-Lvzdh1ewXDQeoaYo,7092
|
55
|
-
pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8pLI,1325
|
56
|
-
pyplumio/structures/temperatures.py,sha256=xm_UETl0tqhoIeIjEESQQkAvE3oU5zjJLxgek2SqEag,2839
|
57
|
-
pyplumio/structures/thermostat_parameters.py,sha256=5xlsDkb0Bh5pAodZDiLp3ojM__mWAz4XQ3fmwfs-o0c,3051
|
58
|
-
pyplumio/structures/thermostat_sensors.py,sha256=oUcyTPMuq8GpPc2HnVh2wpNC5QNNMj82WF6bsU58MFA,3407
|
59
|
-
pyplumio-0.5.43.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
60
|
-
pyplumio-0.5.43.dist-info/METADATA,sha256=ZnTjSCqiyRG_d_siYFQn5XRvGLRUT28gsg7Rswo0hU4,5510
|
61
|
-
pyplumio-0.5.43.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
62
|
-
pyplumio-0.5.43.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
63
|
-
pyplumio-0.5.43.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|