PyPlumIO 0.5.47__py3-none-any.whl → 0.5.49__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/devices/__init__.py +2 -2
- pyplumio/devices/ecomax.py +2 -2
- pyplumio/devices/mixer.py +2 -2
- pyplumio/devices/thermostat.py +2 -2
- pyplumio/filters.py +62 -7
- pyplumio/helpers/async_cache.py +3 -2
- pyplumio/helpers/factory.py +9 -13
- pyplumio/parameters/__init__.py +3 -60
- pyplumio/parameters/custom/__init__.py +111 -0
- pyplumio/parameters/custom/ecomax_860d3_hb.py +80 -0
- pyplumio/parameters/ecomax.py +7 -54
- {pyplumio-0.5.47.dist-info → pyplumio-0.5.49.dist-info}/METADATA +2 -2
- {pyplumio-0.5.47.dist-info → pyplumio-0.5.49.dist-info}/RECORD +17 -15
- {pyplumio-0.5.47.dist-info → pyplumio-0.5.49.dist-info}/WHEEL +1 -1
- {pyplumio-0.5.47.dist-info → pyplumio-0.5.49.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.47.dist-info → pyplumio-0.5.49.dist-info}/top_level.txt +0 -0
pyplumio/_version.py
CHANGED
pyplumio/devices/__init__.py
CHANGED
@@ -72,7 +72,7 @@ class Device(ABC, EventManager):
|
|
72
72
|
:param name: Name of the parameter
|
73
73
|
:type name: str
|
74
74
|
:param value: New value for the parameter
|
75
|
-
:type value: int | float | bool | Literal["
|
75
|
+
:type value: int | float | bool | Literal["on", "off"]
|
76
76
|
:param retries: Try setting parameter for this amount of
|
77
77
|
times, defaults to 5
|
78
78
|
:type retries: int, optional
|
@@ -104,7 +104,7 @@ class Device(ABC, EventManager):
|
|
104
104
|
:param name: Name of the parameter
|
105
105
|
:type name: str
|
106
106
|
:param value: New value for the parameter
|
107
|
-
:type value: int | float | bool | Literal["
|
107
|
+
:type value: int | float | bool | Literal["on", "off"]
|
108
108
|
:param retries: Try setting parameter for this amount of
|
109
109
|
times, defaults to 5
|
110
110
|
:type retries: int, optional
|
pyplumio/devices/ecomax.py
CHANGED
@@ -260,10 +260,10 @@ class EcoMAX(PhysicalDevice):
|
|
260
260
|
"""Update ecoMAX parameters and dispatch the events."""
|
261
261
|
_LOGGER.info("Received device parameters")
|
262
262
|
product_info: ProductInfo = await self.get(ATTR_PRODUCT)
|
263
|
+
parameter_types = await get_ecomax_parameter_types(product_info)
|
263
264
|
|
264
|
-
def _ecomax_parameter_events() -> Generator[Coroutine
|
265
|
+
def _ecomax_parameter_events() -> Generator[Coroutine]:
|
265
266
|
"""Get dispatch calls for ecoMAX parameter events."""
|
266
|
-
parameter_types = get_ecomax_parameter_types(product_info)
|
267
267
|
for index, values in parameters:
|
268
268
|
try:
|
269
269
|
description = parameter_types[index]
|
pyplumio/devices/mixer.py
CHANGED
@@ -42,10 +42,10 @@ class Mixer(VirtualDevice):
|
|
42
42
|
"""Update mixer parameters and dispatch the events."""
|
43
43
|
_LOGGER.info("Received mixer %i parameters", self.index)
|
44
44
|
product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
|
45
|
+
parameter_types = get_mixer_parameter_types(product_info)
|
45
46
|
|
46
|
-
def _mixer_parameter_events() -> Generator[Coroutine
|
47
|
+
def _mixer_parameter_events() -> Generator[Coroutine]:
|
47
48
|
"""Get dispatch calls for mixer parameter events."""
|
48
|
-
parameter_types = get_mixer_parameter_types(product_info)
|
49
49
|
for index, values in parameters:
|
50
50
|
try:
|
51
51
|
description = parameter_types[index]
|
pyplumio/devices/thermostat.py
CHANGED
@@ -40,10 +40,10 @@ class Thermostat(VirtualDevice):
|
|
40
40
|
) -> bool:
|
41
41
|
"""Update thermostat parameters and dispatch the events."""
|
42
42
|
_LOGGER.info("Received thermostat %i parameters", self.index)
|
43
|
+
parameter_types = get_thermostat_parameter_types()
|
43
44
|
|
44
|
-
def _thermostat_parameter_events() -> Generator[Coroutine
|
45
|
+
def _thermostat_parameter_events() -> Generator[Coroutine]:
|
45
46
|
"""Get dispatch calls for thermostat parameter events."""
|
46
|
-
parameter_types = get_thermostat_parameter_types()
|
47
47
|
for index, values in parameters:
|
48
48
|
description = parameter_types[index]
|
49
49
|
handler = (
|
pyplumio/filters.py
CHANGED
@@ -36,7 +36,6 @@ with suppress(ImportError):
|
|
36
36
|
|
37
37
|
|
38
38
|
UNDEFINED: Final = "undefined"
|
39
|
-
TOLERANCE: Final = 0.1
|
40
39
|
|
41
40
|
|
42
41
|
@runtime_checkable
|
@@ -63,26 +62,34 @@ class SupportsComparison(Protocol):
|
|
63
62
|
|
64
63
|
Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
|
65
64
|
|
65
|
+
DEFAULT_TOLERANCE: Final = 1e-6
|
66
|
+
|
66
67
|
|
67
68
|
@overload
|
68
|
-
def is_close(old: Parameter, new: Parameter) -> bool: ...
|
69
|
+
def is_close(old: Parameter, new: Parameter, tolerance: None = None) -> bool: ...
|
69
70
|
|
70
71
|
|
71
72
|
@overload
|
72
|
-
def is_close(
|
73
|
+
def is_close(
|
74
|
+
old: SupportsFloat, new: SupportsFloat, tolerance: float = DEFAULT_TOLERANCE
|
75
|
+
) -> bool: ...
|
73
76
|
|
74
77
|
|
75
78
|
@overload
|
76
|
-
def is_close(
|
79
|
+
def is_close(
|
80
|
+
old: SupportsComparison, new: SupportsComparison, tolerance: None = None
|
81
|
+
) -> bool: ...
|
77
82
|
|
78
83
|
|
79
|
-
def is_close(
|
84
|
+
def is_close(
|
85
|
+
old: Comparable, new: Comparable, tolerance: float | None = DEFAULT_TOLERANCE
|
86
|
+
) -> bool:
|
80
87
|
"""Check if value is significantly changed."""
|
81
88
|
if isinstance(old, Parameter) and isinstance(new, Parameter):
|
82
89
|
return new.pending_update or old.values.__ne__(new.values)
|
83
90
|
|
84
|
-
if isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
85
|
-
return not math.isclose(old, new, abs_tol=
|
91
|
+
if tolerance and isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
92
|
+
return not math.isclose(old, new, abs_tol=tolerance)
|
86
93
|
|
87
94
|
return old.__ne__(new)
|
88
95
|
|
@@ -293,6 +300,53 @@ def custom(callback: Callback, filter_fn: _FilterT) -> _Custom:
|
|
293
300
|
return _Custom(callback, filter_fn)
|
294
301
|
|
295
302
|
|
303
|
+
class _Deadband(Filter):
|
304
|
+
"""Represents a deadband filter.
|
305
|
+
|
306
|
+
Calls a callback only when value is significantly changed from the
|
307
|
+
previous callback call.
|
308
|
+
"""
|
309
|
+
|
310
|
+
__slots__ = ("_tolerance",)
|
311
|
+
|
312
|
+
_tolerance: float
|
313
|
+
|
314
|
+
def __init__(self, callback: Callback, tolerance: float) -> None:
|
315
|
+
"""Initialize a new value changed filter."""
|
316
|
+
self._tolerance = tolerance
|
317
|
+
super().__init__(callback)
|
318
|
+
|
319
|
+
async def __call__(self, new_value: Any) -> Any:
|
320
|
+
"""Set a new value for the callback."""
|
321
|
+
if not isinstance(new_value, (float, int, Decimal)):
|
322
|
+
raise TypeError(
|
323
|
+
"Deadband filter can only be used with numeric values, got "
|
324
|
+
f"{type(new_value).__name__}: {new_value}"
|
325
|
+
)
|
326
|
+
|
327
|
+
if self._value == UNDEFINED or is_close(
|
328
|
+
self._value, new_value, tolerance=self._tolerance
|
329
|
+
):
|
330
|
+
self._value = new_value
|
331
|
+
return await self._callback(new_value)
|
332
|
+
|
333
|
+
|
334
|
+
def deadband(callback: Callback, tolerance: float) -> _Deadband:
|
335
|
+
"""Create a new deadband filter.
|
336
|
+
|
337
|
+
A callback function will only be called when the value is significantly changed
|
338
|
+
from the previous callback call.
|
339
|
+
|
340
|
+
:param callback: A callback function to be awaited on significant value change
|
341
|
+
:type callback: Callback
|
342
|
+
:param tolerance: The minimum difference required to trigger the callback
|
343
|
+
:type tolerance: float
|
344
|
+
:return: An instance of callable filter
|
345
|
+
:rtype: _Deadband
|
346
|
+
"""
|
347
|
+
return _Deadband(callback, tolerance)
|
348
|
+
|
349
|
+
|
296
350
|
class _Debounce(Filter):
|
297
351
|
"""Represents a debounce filter.
|
298
352
|
|
@@ -468,6 +522,7 @@ __all__ = [
|
|
468
522
|
"aggregate",
|
469
523
|
"clamp",
|
470
524
|
"custom",
|
525
|
+
"deadband",
|
471
526
|
"debounce",
|
472
527
|
"delta",
|
473
528
|
"on_change",
|
pyplumio/helpers/async_cache.py
CHANGED
@@ -4,10 +4,11 @@ from collections.abc import Awaitable
|
|
4
4
|
from functools import wraps
|
5
5
|
from typing import Any, Callable, TypeVar, cast
|
6
6
|
|
7
|
-
from typing_extensions import ParamSpec
|
7
|
+
from typing_extensions import ParamSpec, TypeAlias
|
8
8
|
|
9
9
|
T = TypeVar("T")
|
10
10
|
P = ParamSpec("P")
|
11
|
+
_CallableT: TypeAlias = Callable[..., Awaitable[Any]]
|
11
12
|
|
12
13
|
|
13
14
|
class AsyncCache:
|
@@ -21,7 +22,7 @@ class AsyncCache:
|
|
21
22
|
"""Initialize the cache."""
|
22
23
|
self.cache = {}
|
23
24
|
|
24
|
-
async def get(self, key: str, coro:
|
25
|
+
async def get(self, key: str, coro: _CallableT) -> Any:
|
25
26
|
"""Get a value from the cache or compute and store it."""
|
26
27
|
if key not in self.cache:
|
27
28
|
self.cache[key] = await coro()
|
pyplumio/helpers/factory.py
CHANGED
@@ -25,19 +25,15 @@ async def import_module(name: str) -> ModuleType:
|
|
25
25
|
async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
|
26
26
|
"""Return a class instance from the class path."""
|
27
27
|
module_name, class_name = class_path.rsplit(".", 1)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
return instance
|
38
|
-
except Exception:
|
39
|
-
_LOGGER.exception("Failed to create instance for class path '%s'", class_path)
|
40
|
-
raise
|
28
|
+
module = await import_module(module_name)
|
29
|
+
instance = getattr(module, class_name)(**kwargs)
|
30
|
+
if not isinstance(instance, cls):
|
31
|
+
raise TypeError(
|
32
|
+
f"Expected instance of '{cls.__name__}', but got "
|
33
|
+
f"'{type(instance).__name__}' from '{class_name}'"
|
34
|
+
)
|
35
|
+
|
36
|
+
return instance
|
41
37
|
|
42
38
|
|
43
39
|
__all__ = ["create_instance"]
|
pyplumio/parameters/__init__.py
CHANGED
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
|
-
from collections.abc import Sequence
|
8
7
|
from dataclasses import dataclass
|
9
8
|
import logging
|
10
9
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
@@ -12,16 +11,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
|
12
11
|
from dataslots import dataslots
|
13
12
|
from typing_extensions import TypeAlias
|
14
13
|
|
15
|
-
from pyplumio.const import
|
16
|
-
BYTE_UNDEFINED,
|
17
|
-
STATE_OFF,
|
18
|
-
STATE_ON,
|
19
|
-
ProductModel,
|
20
|
-
State,
|
21
|
-
UnitOfMeasurement,
|
22
|
-
)
|
14
|
+
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
|
23
15
|
from pyplumio.frames import Request
|
24
|
-
from pyplumio.structures.product_info import ProductInfo
|
25
16
|
from pyplumio.utils import is_divisible
|
26
17
|
|
27
18
|
if TYPE_CHECKING:
|
@@ -120,8 +111,8 @@ class Parameter(ABC):
|
|
120
111
|
other = other.values
|
121
112
|
|
122
113
|
if isinstance(other, ParameterValues):
|
123
|
-
handler = getattr(self.values, method_to_call)
|
124
|
-
return handler(other)
|
114
|
+
handler = getattr(self.values.value, method_to_call)
|
115
|
+
return handler(other.value)
|
125
116
|
|
126
117
|
if isinstance(other, (int, float, bool)) or other in get_args(State):
|
127
118
|
handler = getattr(self.values.value, method_to_call)
|
@@ -486,53 +477,6 @@ class Switch(Parameter):
|
|
486
477
|
return STATE_ON
|
487
478
|
|
488
479
|
|
489
|
-
@dataclass
|
490
|
-
class ParameterOverride:
|
491
|
-
"""Represents a parameter override."""
|
492
|
-
|
493
|
-
__slot__ = ("original", "replacement", "product_model", "product_id")
|
494
|
-
|
495
|
-
original: str
|
496
|
-
replacement: ParameterDescription
|
497
|
-
product_model: ProductModel
|
498
|
-
product_id: int
|
499
|
-
|
500
|
-
|
501
|
-
_DescriptorT = TypeVar("_DescriptorT", bound=ParameterDescription)
|
502
|
-
|
503
|
-
|
504
|
-
def patch_parameter_types(
|
505
|
-
product_info: ProductInfo,
|
506
|
-
parameter_types: list[_DescriptorT],
|
507
|
-
parameter_overrides: Sequence[ParameterOverride],
|
508
|
-
) -> list[_DescriptorT]:
|
509
|
-
"""Patch the parameter types based on the provided overrides.
|
510
|
-
|
511
|
-
Note:
|
512
|
-
The `# type: ignore[assignment]` comment is used to suppress a
|
513
|
-
type-checking error caused by mypy bug. For more details, see:
|
514
|
-
https://github.com/python/mypy/issues/13596
|
515
|
-
|
516
|
-
"""
|
517
|
-
replacements = {
|
518
|
-
override.original: override.replacement
|
519
|
-
for override in parameter_overrides
|
520
|
-
if override.product_model.value == product_info.model
|
521
|
-
and override.product_id == product_info.id
|
522
|
-
}
|
523
|
-
for index, description in enumerate(parameter_types):
|
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
|
-
)
|
531
|
-
parameter_types[index] = replacements[description.name] # type: ignore[assignment]
|
532
|
-
|
533
|
-
return parameter_types
|
534
|
-
|
535
|
-
|
536
480
|
__all__ = [
|
537
481
|
"Number",
|
538
482
|
"NumberDescription",
|
@@ -542,7 +486,6 @@ __all__ = [
|
|
542
486
|
"Parameter",
|
543
487
|
"ParameterDescription",
|
544
488
|
"ParameterValues",
|
545
|
-
"patch_parameter_types",
|
546
489
|
"State",
|
547
490
|
"Switch",
|
548
491
|
"SwitchDescription",
|
@@ -0,0 +1,111 @@
|
|
1
|
+
"""Custom parameters for products."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections.abc import Sequence
|
6
|
+
from dataclasses import dataclass
|
7
|
+
import logging
|
8
|
+
from typing import ClassVar, TypeVar, cast
|
9
|
+
|
10
|
+
from pyplumio.helpers.factory import create_instance
|
11
|
+
from pyplumio.parameters import ParameterDescription
|
12
|
+
from pyplumio.structures.product_info import ProductInfo
|
13
|
+
from pyplumio.utils import to_camelcase
|
14
|
+
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Signature:
|
20
|
+
"""Represents a product signature."""
|
21
|
+
|
22
|
+
__slots__ = ("id", "model")
|
23
|
+
|
24
|
+
id: int
|
25
|
+
model: str
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class CustomParameter:
|
30
|
+
"""Represents a custom parameter."""
|
31
|
+
|
32
|
+
__slot__ = ("original", "replacement")
|
33
|
+
|
34
|
+
original: str
|
35
|
+
replacement: ParameterDescription
|
36
|
+
|
37
|
+
|
38
|
+
class CustomParameters:
|
39
|
+
"""Represents a custom parameters."""
|
40
|
+
|
41
|
+
__slots__ = ("signature", "replacements")
|
42
|
+
|
43
|
+
signature: ClassVar[Signature]
|
44
|
+
replacements: ClassVar[Sequence[CustomParameter]]
|
45
|
+
|
46
|
+
def validate(self, product_info: ProductInfo) -> bool:
|
47
|
+
"""Validate the product info."""
|
48
|
+
return (
|
49
|
+
self.signature.id == product_info.id
|
50
|
+
and self.signature.model == product_info.model
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
async def _load_custom_parameters(
|
55
|
+
product_info: ProductInfo,
|
56
|
+
) -> dict[str, ParameterDescription] | None:
|
57
|
+
"""Load custom parameters."""
|
58
|
+
module_name = product_info.model.replace("-", "_").replace(" ", "_").lower()
|
59
|
+
module_path = f"parameters.custom.{module_name}"
|
60
|
+
class_name = to_camelcase(module_name).upper().replace("ECOMAX", "EcoMAX")
|
61
|
+
class_path = f"{module_path}.{class_name}"
|
62
|
+
try:
|
63
|
+
_LOGGER.debug(
|
64
|
+
"Trying to load custom parameters for %s from %s",
|
65
|
+
product_info.model,
|
66
|
+
class_path,
|
67
|
+
)
|
68
|
+
custom_parameters = await create_instance(class_path, cls=CustomParameters)
|
69
|
+
if not custom_parameters.validate(product_info):
|
70
|
+
raise ValueError
|
71
|
+
except (ImportError, TypeError, ValueError):
|
72
|
+
_LOGGER.debug("No custom parameters found for %s", product_info.model)
|
73
|
+
return None
|
74
|
+
|
75
|
+
return {
|
76
|
+
custom_parameter.original: custom_parameter.replacement
|
77
|
+
for custom_parameter in custom_parameters.replacements
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
_DescriptionT = TypeVar("_DescriptionT", bound=ParameterDescription)
|
82
|
+
|
83
|
+
|
84
|
+
async def inject_custom_parameters(
|
85
|
+
product_info: ProductInfo, parameter_types: list[_DescriptionT]
|
86
|
+
) -> list[_DescriptionT]:
|
87
|
+
"""Patch the parameter types based on the provided overrides."""
|
88
|
+
if custom_parameters := await _load_custom_parameters(product_info):
|
89
|
+
_LOGGER.debug("Custom parameters found for %s", product_info.model)
|
90
|
+
return cast(
|
91
|
+
list[_DescriptionT],
|
92
|
+
[
|
93
|
+
replacement
|
94
|
+
if original.name in custom_parameters
|
95
|
+
and (replacement := custom_parameters[original.name])
|
96
|
+
and (base_class := original.__class__.__bases__[0])
|
97
|
+
and isinstance(replacement, base_class)
|
98
|
+
else original
|
99
|
+
for original in parameter_types
|
100
|
+
],
|
101
|
+
)
|
102
|
+
|
103
|
+
return parameter_types
|
104
|
+
|
105
|
+
|
106
|
+
__all__ = (
|
107
|
+
"inject_custom_parameters",
|
108
|
+
"CustomParameters",
|
109
|
+
"CustomParameter",
|
110
|
+
"Signature",
|
111
|
+
)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"""Contains patch for ecoMAX 860D3-HB."""
|
2
|
+
|
3
|
+
from pyplumio.const import UnitOfMeasurement
|
4
|
+
from pyplumio.parameters.custom import CustomParameter, CustomParameters, Signature
|
5
|
+
from pyplumio.parameters.ecomax import EcomaxNumberDescription, EcomaxSwitchDescription
|
6
|
+
|
7
|
+
|
8
|
+
class EcoMAX860D3HB(CustomParameters):
|
9
|
+
"""Replacements for ecoMAX 860D3-HB."""
|
10
|
+
|
11
|
+
__slots__ = ()
|
12
|
+
|
13
|
+
signature = Signature(model="ecoMAX 860D3-HB", id=48)
|
14
|
+
|
15
|
+
replacements = (
|
16
|
+
# Summer mode
|
17
|
+
CustomParameter(
|
18
|
+
original="summer_mode_disable_temp",
|
19
|
+
replacement=EcomaxNumberDescription(name="__unknown_parameter_1"),
|
20
|
+
),
|
21
|
+
CustomParameter(
|
22
|
+
original="water_heater_target_temp",
|
23
|
+
replacement=EcomaxNumberDescription(name="summer_mode"),
|
24
|
+
),
|
25
|
+
CustomParameter(
|
26
|
+
original="min_water_heater_target_temp",
|
27
|
+
replacement=EcomaxNumberDescription(
|
28
|
+
name="summer_mode_enable_temp",
|
29
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
30
|
+
),
|
31
|
+
),
|
32
|
+
CustomParameter(
|
33
|
+
original="max_water_heater_target_temp",
|
34
|
+
replacement=EcomaxNumberDescription(
|
35
|
+
name="summer_mode_disable_temp",
|
36
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
37
|
+
),
|
38
|
+
),
|
39
|
+
# Water heater
|
40
|
+
CustomParameter(
|
41
|
+
original="disable_pump_on_thermostat",
|
42
|
+
replacement=EcomaxNumberDescription(
|
43
|
+
name="water_heater_target_temp",
|
44
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
45
|
+
),
|
46
|
+
),
|
47
|
+
CustomParameter(
|
48
|
+
original="boiler_alert_temp",
|
49
|
+
replacement=EcomaxNumberDescription(
|
50
|
+
name="min_water_heater_target_temp",
|
51
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
52
|
+
),
|
53
|
+
),
|
54
|
+
CustomParameter(
|
55
|
+
original="max_feeder_temp",
|
56
|
+
replacement=EcomaxNumberDescription(
|
57
|
+
name="max_water_heater_target_temp",
|
58
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
59
|
+
),
|
60
|
+
),
|
61
|
+
CustomParameter(
|
62
|
+
original="water_heater_work_mode",
|
63
|
+
replacement=EcomaxNumberDescription(name="water_heater_feeding_extension"),
|
64
|
+
),
|
65
|
+
CustomParameter(
|
66
|
+
original="external_boiler_temp",
|
67
|
+
replacement=EcomaxNumberDescription(name="water_heater_work_mode"),
|
68
|
+
),
|
69
|
+
CustomParameter(
|
70
|
+
original="alert_notify",
|
71
|
+
replacement=EcomaxNumberDescription(
|
72
|
+
name="water_heater_hysteresis",
|
73
|
+
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
74
|
+
),
|
75
|
+
),
|
76
|
+
CustomParameter(
|
77
|
+
original="pump_hysteresis",
|
78
|
+
replacement=EcomaxSwitchDescription(name="water_heater_disinfection"),
|
79
|
+
),
|
80
|
+
)
|
pyplumio/parameters/ecomax.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from dataclasses import dataclass
|
6
|
-
from functools import
|
6
|
+
from functools import partial
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
9
|
from dataslots import dataslots
|
@@ -15,21 +15,20 @@ from pyplumio.const import (
|
|
15
15
|
ATTR_VALUE,
|
16
16
|
PERCENTAGE,
|
17
17
|
FrameType,
|
18
|
-
ProductModel,
|
19
18
|
ProductType,
|
20
19
|
UnitOfMeasurement,
|
21
20
|
)
|
22
21
|
from pyplumio.frames import Request
|
22
|
+
from pyplumio.helpers.async_cache import acache
|
23
23
|
from pyplumio.parameters import (
|
24
24
|
OffsetNumber,
|
25
25
|
OffsetNumberDescription,
|
26
26
|
Parameter,
|
27
27
|
ParameterDescription,
|
28
|
-
ParameterOverride,
|
29
28
|
Switch,
|
30
29
|
SwitchDescription,
|
31
|
-
patch_parameter_types,
|
32
30
|
)
|
31
|
+
from pyplumio.parameters.custom import inject_custom_parameters
|
33
32
|
from pyplumio.structures.ecomax_parameters import ATTR_ECOMAX_CONTROL
|
34
33
|
from pyplumio.structures.product_info import ProductInfo
|
35
34
|
from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PROFILE
|
@@ -798,58 +797,13 @@ ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
|
|
798
797
|
THERMOSTAT_PROFILE_PARAMETER = EcomaxNumberDescription(name=ATTR_THERMOSTAT_PROFILE)
|
799
798
|
|
800
799
|
|
801
|
-
@
|
802
|
-
|
803
|
-
"""Represents an ecoMAX parameter override."""
|
804
|
-
|
805
|
-
__slots__ = ()
|
806
|
-
|
807
|
-
replacement: EcomaxParameterDescription
|
808
|
-
|
809
|
-
|
810
|
-
PARAMETER_OVERRIDES: tuple[EcomaxParameterOverride, ...] = (
|
811
|
-
EcomaxParameterOverride(
|
812
|
-
original="summer_mode_disable_temp",
|
813
|
-
replacement=EcomaxNumberDescription(name="__unknown_parameter_1"),
|
814
|
-
product_model=ProductModel.ECOMAX_860D3_HB,
|
815
|
-
product_id=48,
|
816
|
-
),
|
817
|
-
EcomaxParameterOverride(
|
818
|
-
original="water_heater_target_temp",
|
819
|
-
replacement=EcomaxNumberDescription(name="summer_mode"),
|
820
|
-
product_model=ProductModel.ECOMAX_860D3_HB,
|
821
|
-
product_id=48,
|
822
|
-
),
|
823
|
-
EcomaxParameterOverride(
|
824
|
-
original="min_water_heater_target_temp",
|
825
|
-
replacement=EcomaxNumberDescription(
|
826
|
-
name="summer_mode_enable_temp",
|
827
|
-
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
828
|
-
),
|
829
|
-
product_model=ProductModel.ECOMAX_860D3_HB,
|
830
|
-
product_id=48,
|
831
|
-
),
|
832
|
-
EcomaxParameterOverride(
|
833
|
-
original="max_water_heater_target_temp",
|
834
|
-
replacement=EcomaxNumberDescription(
|
835
|
-
name="summer_mode_disable_temp",
|
836
|
-
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
837
|
-
),
|
838
|
-
product_model=ProductModel.ECOMAX_860D3_HB,
|
839
|
-
product_id=48,
|
840
|
-
),
|
841
|
-
)
|
842
|
-
|
843
|
-
|
844
|
-
@cache
|
845
|
-
def get_ecomax_parameter_types(
|
800
|
+
@acache
|
801
|
+
async def get_ecomax_parameter_types(
|
846
802
|
product_info: ProductInfo,
|
847
803
|
) -> list[EcomaxParameterDescription]:
|
848
804
|
"""Return ecoMAX parameter types for specific product."""
|
849
|
-
return
|
850
|
-
product_info,
|
851
|
-
parameter_types=PARAMETER_TYPES[product_info.type],
|
852
|
-
parameter_overrides=PARAMETER_OVERRIDES,
|
805
|
+
return await inject_custom_parameters(
|
806
|
+
product_info, parameter_types=PARAMETER_TYPES[product_info.type]
|
853
807
|
)
|
854
808
|
|
855
809
|
|
@@ -862,7 +816,6 @@ __all__ = [
|
|
862
816
|
"EcomaxSwitch",
|
863
817
|
"EcomaxSwitchDescription",
|
864
818
|
"get_ecomax_parameter_types",
|
865
|
-
"PARAMETER_OVERRIDES",
|
866
819
|
"PARAMETER_TYPES",
|
867
820
|
"THERMOSTAT_PROFILE_PARAMETER",
|
868
821
|
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.49
|
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
|
@@ -35,7 +35,7 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
|
35
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
36
|
Requires-Dist: pytest==8.3.5; extra == "test"
|
37
37
|
Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
|
38
|
-
Requires-Dist: ruff==0.11.
|
38
|
+
Requires-Dist: ruff==0.11.9; extra == "test"
|
39
39
|
Requires-Dist: tox==4.25.0; extra == "test"
|
40
40
|
Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
|
41
41
|
Provides-Extra: docs
|
@@ -1,36 +1,38 @@
|
|
1
1
|
pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
|
2
2
|
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
-
pyplumio/_version.py,sha256=
|
3
|
+
pyplumio/_version.py,sha256=u94vlma_2aQlFbifB92wUyxJiUQkXGcFrFlXXxDUYBA,513
|
4
4
|
pyplumio/connection.py,sha256=9MCPb8W62uqCrzd1YCROcn9cCjRY8E65934FnJDF5Js,5902
|
5
5
|
pyplumio/const.py,sha256=FxF97bl_GunYuB8Wo72zCzHtznRCM64ygC2qfaR3UyA,5684
|
6
6
|
pyplumio/data_types.py,sha256=r-QOIZiIpBFo4kRongyu8n0BHTaEU6wWMTmNkWBNjq8,9223
|
7
7
|
pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
|
8
|
-
pyplumio/filters.py,sha256=
|
8
|
+
pyplumio/filters.py,sha256=s35hmm8HxqktMOtNZMdXbqMeywBNKoIsAc5hWd0AEpk,15391
|
9
9
|
pyplumio/protocol.py,sha256=UnrXDouo4dDi7hqwJYHoEAvCoYJs3PgP1DFBBwRqBrw,8427
|
10
10
|
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
pyplumio/stream.py,sha256=iCB-XlNXRRE0p7nstb1AkXwVDwcCsgym_ggB_IDiJc8,4926
|
12
12
|
pyplumio/utils.py,sha256=D6_SJzYkFjXoUrlNPt_mIQAP8hjMU05RsTqlAFphj3Y,1205
|
13
|
-
pyplumio/devices/__init__.py,sha256=
|
14
|
-
pyplumio/devices/ecomax.py,sha256
|
13
|
+
pyplumio/devices/__init__.py,sha256=frEoioDkJJuSGoBDlW42qxsRN_8lpzOvNJH56YQpd9E,7999
|
14
|
+
pyplumio/devices/ecomax.py,sha256=-4rVUEg5MbzzoNXOFjp4cShsf4cqFh0mlIqlloZyWz4,16191
|
15
15
|
pyplumio/devices/ecoster.py,sha256=X46ky5XT8jHMFq9sBW0ve8ZI_tjItQDMt4moXsW-ogY,307
|
16
|
-
pyplumio/devices/mixer.py,sha256=
|
17
|
-
pyplumio/devices/thermostat.py,sha256=
|
16
|
+
pyplumio/devices/mixer.py,sha256=q2UXMCQl6gueigHt-1Y1_P1sTEbBisZeQC75u2zAKmc,2729
|
17
|
+
pyplumio/devices/thermostat.py,sha256=LBHJHfbkJgrwAifILk_IW_rRuDzSX1CDnA2xcLh4-NI,2251
|
18
18
|
pyplumio/frames/__init__.py,sha256=5lw19oFlN89ZvO8KGwnkwERULQNYiP-hhZKk65LsjYY,7862
|
19
19
|
pyplumio/frames/messages.py,sha256=ImQGWFFTa2eaXfytQmFZKC-IxyPRkxD8qp0bEm16-ws,3628
|
20
20
|
pyplumio/frames/requests.py,sha256=jr-_XSSCCDDTbAmrw95CKyWa5nb7JNeGzZ2jDXIxlAo,7348
|
21
21
|
pyplumio/frames/responses.py,sha256=M6Ky4gg2AoShmRXX0x6nftajxrvmQLKPVRWbwyhvI0E,6663
|
22
22
|
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
23
|
-
pyplumio/helpers/async_cache.py,sha256=
|
23
|
+
pyplumio/helpers/async_cache.py,sha256=EGQcU8LWJpVx3Hk6iaI-3mqAhnR5ACfBOGb9tWw-VSY,1305
|
24
24
|
pyplumio/helpers/event_manager.py,sha256=aKNlhsPNTy3eOSfWVb9TJxtIsN9GAQv9XxhOi_BOhlM,8097
|
25
|
-
pyplumio/helpers/factory.py,sha256=
|
25
|
+
pyplumio/helpers/factory.py,sha256=c3sitnkUjJWz7fPpTE9uRIpa8h46Qim3xsAblMw3eDo,1049
|
26
26
|
pyplumio/helpers/schedule.py,sha256=ODNfMuqRZuFnnFxzFFvbE0sSQ6sxp4EUyxPMDBym-L0,5308
|
27
27
|
pyplumio/helpers/task_manager.py,sha256=N71F6Ag1HHxdf5zJeCMcEziFdH9lmJKtMPoRGjJ-E40,1209
|
28
28
|
pyplumio/helpers/timeout.py,sha256=2wAcOug-2TgdCchKbfv0VhAfNzP-MPM0TEFtRNFJ_m8,803
|
29
29
|
pyplumio/helpers/uid.py,sha256=HeM5Zmd0qfNmVya6RKE-bBYzdxG-pAiViOZRHqw33VU,1011
|
30
|
-
pyplumio/parameters/__init__.py,sha256=
|
31
|
-
pyplumio/parameters/ecomax.py,sha256=
|
30
|
+
pyplumio/parameters/__init__.py,sha256=g9AWuCgEByGpLaiXRqE8f9xZ208ut2DNhEiDUJ0SJhs,14963
|
31
|
+
pyplumio/parameters/ecomax.py,sha256=4UqI7cokCt7qS9Du4-7JgLhM7mhHCHt8SWPl_qncmXQ,26239
|
32
32
|
pyplumio/parameters/mixer.py,sha256=RjhfUU_62STyNV0Ud9A4G4FEvVwo02qGVl8h1QvqQXI,6646
|
33
33
|
pyplumio/parameters/thermostat.py,sha256=-DK2Mb78CGrKmdhwAD0M3GiGJatczPnl1e2gVeT19tI,5070
|
34
|
+
pyplumio/parameters/custom/__init__.py,sha256=tI_MXv0ExI8do7nPjA3oWiu0m0alGLlctggNNrgq09c,3240
|
35
|
+
pyplumio/parameters/custom/ecomax_860d3_hb.py,sha256=DgPm-o2iOxveLR-2KTrf5xC9KRVJxBHK_3cQV4UNsEs,2860
|
34
36
|
pyplumio/structures/__init__.py,sha256=emZVH5OFgdTUPbEJoznMKitmK0nlPm0I4SmF86It1Do,1345
|
35
37
|
pyplumio/structures/alerts.py,sha256=fglFcxHoZ3hZJscPHmbJJqTr2mH8YXRW0T18L4Dirds,3692
|
36
38
|
pyplumio/structures/boiler_load.py,sha256=e-6itp9L6iJeeOyhSTiOclHLuYmqG7KkcepsHwJSQSI,894
|
@@ -57,8 +59,8 @@ pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8p
|
|
57
59
|
pyplumio/structures/temperatures.py,sha256=2VD3P_vwp9PEBkOn2-WhifOR8w-UYNq35aAxle0z2Vg,2831
|
58
60
|
pyplumio/structures/thermostat_parameters.py,sha256=st3x3HkjQm3hqBrn_fpvPDQu8fuc-Sx33ONB19ViQak,3007
|
59
61
|
pyplumio/structures/thermostat_sensors.py,sha256=rO9jTZWGQpThtJqVdbbv8sYMYHxJi4MfwZQza69L2zw,3399
|
60
|
-
pyplumio-0.5.
|
61
|
-
pyplumio-0.5.
|
62
|
-
pyplumio-0.5.
|
63
|
-
pyplumio-0.5.
|
64
|
-
pyplumio-0.5.
|
62
|
+
pyplumio-0.5.49.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
63
|
+
pyplumio-0.5.49.dist-info/METADATA,sha256=dmnFwYN07jJBW5VBjNpme32Y7DbbAp1M9FPDn4AuRJA,5611
|
64
|
+
pyplumio-0.5.49.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
65
|
+
pyplumio-0.5.49.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
66
|
+
pyplumio-0.5.49.dist-info/RECORD,,
|
File without changes
|
File without changes
|