ramses-rf 0.22.40__py3-none-any.whl → 0.51.1__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.
- ramses_cli/__init__.py +18 -0
- ramses_cli/client.py +597 -0
- ramses_cli/debug.py +20 -0
- ramses_cli/discovery.py +405 -0
- ramses_cli/utils/cat_slow.py +17 -0
- ramses_cli/utils/convert.py +60 -0
- ramses_rf/__init__.py +31 -10
- ramses_rf/binding_fsm.py +787 -0
- ramses_rf/const.py +124 -105
- ramses_rf/database.py +297 -0
- ramses_rf/device/__init__.py +69 -39
- ramses_rf/device/base.py +187 -376
- ramses_rf/device/heat.py +540 -552
- ramses_rf/device/hvac.py +286 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +377 -513
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.1.dist-info/METADATA +72 -0
- ramses_rf-0.51.1.dist-info/RECORD +55 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
- ramses_tx/__init__.py +160 -0
- {ramses_rf/protocol → ramses_tx}/address.py +65 -59
- ramses_tx/command.py +1454 -0
- ramses_tx/const.py +903 -0
- ramses_tx/exceptions.py +92 -0
- {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
- {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
- ramses_tx/gateway.py +338 -0
- ramses_tx/helpers.py +883 -0
- {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
- {ramses_rf/protocol → ramses_tx}/message.py +155 -191
- ramses_tx/opentherm.py +1260 -0
- ramses_tx/packet.py +210 -0
- {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1576
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.40.dist-info/METADATA +0 -64
- ramses_rf-0.22.40.dist-info/RECORD +0 -42
- ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_rf/helpers.py
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
2
|
"""RAMSES RF - Helper functions."""
|
|
3
|
+
|
|
5
4
|
from __future__ import annotations
|
|
6
5
|
|
|
7
6
|
import asyncio
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
8
|
from copy import deepcopy
|
|
9
9
|
from inspect import iscoroutinefunction
|
|
10
|
+
from typing import Any, TypeAlias
|
|
11
|
+
|
|
12
|
+
_SchemaT: TypeAlias = dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_subset(inner: _SchemaT, outer: _SchemaT) -> bool:
|
|
16
|
+
"""Return True is one dict (or list) is a subset of another."""
|
|
10
17
|
|
|
18
|
+
def _is_subset(
|
|
19
|
+
a: dict[str, Any] | list[Any] | Any, b: dict[str, Any] | list[Any] | Any
|
|
20
|
+
) -> bool:
|
|
21
|
+
if isinstance(a, dict):
|
|
22
|
+
return isinstance(b, dict) and all(
|
|
23
|
+
k in b and _is_subset(v, b[k]) for k, v in a.items()
|
|
24
|
+
)
|
|
25
|
+
if isinstance(a, list):
|
|
26
|
+
return isinstance(b, list) and all(
|
|
27
|
+
any(_is_subset(x, y) for y in b) for x in a
|
|
28
|
+
)
|
|
29
|
+
return bool(a == b)
|
|
11
30
|
|
|
12
|
-
|
|
31
|
+
return _is_subset(inner, outer)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def deep_merge(src: _SchemaT, dst: _SchemaT, _dc: bool = False) -> _SchemaT:
|
|
13
35
|
"""Deep merge a src dict (precedent) into a dst dict and return the result.
|
|
14
36
|
|
|
15
37
|
run me with nosetests --with-doctest file.py
|
|
@@ -22,13 +44,12 @@ def merge(src: dict, dst: dict, _dc: bool = None) -> dict: # TODO: move to rams
|
|
|
22
44
|
|
|
23
45
|
new_dst = dst if _dc else deepcopy(dst) # start with copy of dst, merge src into it
|
|
24
46
|
for key, value in src.items(): # values are only: dict, list, value or None
|
|
25
|
-
|
|
26
47
|
if isinstance(value, dict): # is dict
|
|
27
48
|
node = new_dst.setdefault(key, {}) # get node or create one
|
|
28
|
-
|
|
49
|
+
deep_merge(value, node, _dc=True)
|
|
29
50
|
|
|
30
51
|
elif not isinstance(value, list): # is value
|
|
31
|
-
new_dst[key] = value # src takes
|
|
52
|
+
new_dst[key] = value # src takes precedence, assert will fail
|
|
32
53
|
|
|
33
54
|
elif key not in new_dst or not isinstance(new_dst[key], list): # is list
|
|
34
55
|
new_dst[key] = src[key] # not expected, but maybe
|
|
@@ -40,7 +61,9 @@ def merge(src: dict, dst: dict, _dc: bool = None) -> dict: # TODO: move to rams
|
|
|
40
61
|
return new_dst
|
|
41
62
|
|
|
42
63
|
|
|
43
|
-
def shrink(
|
|
64
|
+
def shrink(
|
|
65
|
+
value: _SchemaT, keep_falsys: bool = False, keep_hints: bool = False
|
|
66
|
+
) -> _SchemaT:
|
|
44
67
|
"""Return a minimized dict, after removing all the meaningless items.
|
|
45
68
|
|
|
46
69
|
Specifically, removes items with:
|
|
@@ -48,7 +71,7 @@ def shrink(value: dict, keep_falsys: bool = False, keep_hints: bool = False) ->
|
|
|
48
71
|
- falsey values
|
|
49
72
|
"""
|
|
50
73
|
|
|
51
|
-
def walk(node):
|
|
74
|
+
def walk(node: Any) -> Any:
|
|
52
75
|
if isinstance(node, dict):
|
|
53
76
|
return {
|
|
54
77
|
k: walk(v)
|
|
@@ -66,29 +89,44 @@ def shrink(value: dict, keep_falsys: bool = False, keep_hints: bool = False) ->
|
|
|
66
89
|
if not isinstance(value, dict):
|
|
67
90
|
raise TypeError("value is not a dict")
|
|
68
91
|
|
|
69
|
-
|
|
92
|
+
result: _SchemaT = walk(value)
|
|
93
|
+
return result
|
|
70
94
|
|
|
71
95
|
|
|
72
|
-
def schedule_task(
|
|
96
|
+
def schedule_task(
|
|
97
|
+
fnc: Awaitable[Any] | Callable[..., Any],
|
|
98
|
+
*args: Any,
|
|
99
|
+
delay: float | None = None,
|
|
100
|
+
period: float | None = None,
|
|
101
|
+
**kwargs: Any,
|
|
102
|
+
) -> asyncio.Task[Any]:
|
|
73
103
|
"""Start a coro after delay seconds."""
|
|
74
104
|
|
|
75
|
-
async def
|
|
76
|
-
|
|
105
|
+
async def execute_fnc(
|
|
106
|
+
fnc: Awaitable[Any] | Callable[..., Any], *args: Any, **kwargs: Any
|
|
107
|
+
) -> Any:
|
|
108
|
+
if iscoroutinefunction(fnc): # Awaitable, else Callable
|
|
77
109
|
return await fnc(*args, **kwargs)
|
|
78
|
-
return fnc(*args, **kwargs)
|
|
79
|
-
|
|
80
|
-
async def
|
|
110
|
+
return fnc(*args, **kwargs) # type: ignore[operator]
|
|
111
|
+
|
|
112
|
+
async def schedule_fnc(
|
|
113
|
+
fnc: Awaitable[Any] | Callable[..., Any],
|
|
114
|
+
delay: float | None,
|
|
115
|
+
period: float | None,
|
|
116
|
+
*args: Any,
|
|
117
|
+
**kwargs: Any,
|
|
118
|
+
) -> Any:
|
|
81
119
|
if delay:
|
|
82
120
|
await asyncio.sleep(delay)
|
|
83
121
|
|
|
84
122
|
if not period:
|
|
85
|
-
|
|
123
|
+
await execute_fnc(fnc, *args, **kwargs)
|
|
86
124
|
return
|
|
87
125
|
|
|
88
126
|
while period:
|
|
89
|
-
|
|
127
|
+
await execute_fnc(fnc, *args, **kwargs)
|
|
90
128
|
await asyncio.sleep(period)
|
|
91
129
|
|
|
92
|
-
return asyncio.create_task(
|
|
93
|
-
|
|
130
|
+
return asyncio.create_task( # do we need to pass in an event loop?
|
|
131
|
+
schedule_fnc(fnc, delay, period, *args, **kwargs), name=str(fnc)
|
|
94
132
|
)
|
ramses_rf/py.typed
ADDED
|
File without changes
|
ramses_rf/schemas.py
CHANGED
|
@@ -1,43 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
2
|
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
5
3
|
|
|
6
4
|
Schema processor for upper layer.
|
|
7
5
|
"""
|
|
6
|
+
|
|
8
7
|
from __future__ import annotations
|
|
9
8
|
|
|
10
9
|
import logging
|
|
11
10
|
import re
|
|
12
|
-
from
|
|
13
|
-
from typing import
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
14
13
|
|
|
15
|
-
import voluptuous as vol
|
|
14
|
+
import voluptuous as vol
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
DEV_TYPE_MAP,
|
|
23
|
-
DEVICE_ID_REGEX,
|
|
24
|
-
DONT_CREATE_MESSAGES,
|
|
25
|
-
SZ_ZONE_IDX,
|
|
26
|
-
ZON_ROLE_MAP,
|
|
27
|
-
SystemType,
|
|
28
|
-
__dev_mode__,
|
|
29
|
-
)
|
|
30
|
-
from .helpers import shrink
|
|
31
|
-
from .protocol.const import (
|
|
32
|
-
SZ_ACTUATORS,
|
|
33
|
-
SZ_DEVICES,
|
|
16
|
+
# TODO: deprecate re-exporting (via as) in favour of direct imports
|
|
17
|
+
from ramses_tx.const import (
|
|
18
|
+
SZ_ACTUATORS as SZ_ACTUATORS,
|
|
19
|
+
SZ_CONFIG as SZ_CONFIG,
|
|
20
|
+
SZ_DEVICES as SZ_DEVICES,
|
|
34
21
|
SZ_NAME,
|
|
35
|
-
SZ_SENSOR,
|
|
22
|
+
SZ_SENSOR as SZ_SENSOR,
|
|
36
23
|
SZ_ZONE_TYPE,
|
|
37
24
|
SZ_ZONES,
|
|
38
25
|
)
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
|
|
27
|
+
# TODO: deprecate re-exporting (via as) in favour of direct imports
|
|
28
|
+
from ramses_tx.schemas import ( # noqa: F401
|
|
41
29
|
SCH_DEVICE_ID_ANY,
|
|
42
30
|
SCH_DEVICE_ID_APP,
|
|
43
31
|
SCH_DEVICE_ID_BDR,
|
|
@@ -48,51 +36,68 @@ from .protocol.schemas import ( # noqa: F401
|
|
|
48
36
|
SCH_DEVICE_ID_UFC,
|
|
49
37
|
SCH_ENGINE_DICT,
|
|
50
38
|
SCH_GLOBAL_TRAITS_DICT,
|
|
51
|
-
SCH_TRAITS,
|
|
52
|
-
SZ_ALIAS,
|
|
39
|
+
SCH_TRAITS as SCH_TRAITS,
|
|
40
|
+
SZ_ALIAS as SZ_ALIAS,
|
|
53
41
|
SZ_BLOCK_LIST,
|
|
54
|
-
SZ_CLASS,
|
|
42
|
+
SZ_CLASS as SZ_CLASS,
|
|
55
43
|
SZ_DISABLE_SENDING,
|
|
56
44
|
SZ_ENFORCE_KNOWN_LIST,
|
|
57
|
-
SZ_FAKED,
|
|
58
|
-
SZ_KNOWN_LIST,
|
|
45
|
+
SZ_FAKED as SZ_FAKED,
|
|
46
|
+
SZ_KNOWN_LIST as SZ_KNOWN_LIST,
|
|
59
47
|
SZ_PACKET_LOG,
|
|
48
|
+
SZ_SCHEME as SZ_SCHEME,
|
|
49
|
+
DeviceIdT,
|
|
60
50
|
sch_packet_log_dict_factory,
|
|
61
51
|
select_device_filter_mode,
|
|
62
52
|
)
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
from . import exceptions as exc
|
|
55
|
+
|
|
56
|
+
# TODO: deprecate re-exporting (via as) in favour of direct imports
|
|
57
|
+
from .const import (
|
|
58
|
+
DEFAULT_MAX_ZONES as DEFAULT_MAX_ZONES,
|
|
59
|
+
DEV_ROLE_MAP,
|
|
60
|
+
DEV_TYPE_MAP,
|
|
61
|
+
DEVICE_ID_REGEX,
|
|
62
|
+
DONT_CREATE_MESSAGES,
|
|
63
|
+
SZ_ZONE_IDX,
|
|
64
|
+
ZON_ROLE_MAP,
|
|
65
|
+
DevRole,
|
|
66
|
+
DevType,
|
|
67
|
+
SystemType,
|
|
68
|
+
)
|
|
65
69
|
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from .device import Device
|
|
72
|
+
from .gateway import Gateway
|
|
73
|
+
from .system import Evohome
|
|
66
74
|
|
|
67
|
-
DEV_MODE = __dev_mode__ and False
|
|
68
75
|
|
|
69
76
|
_LOGGER = logging.getLogger(__name__)
|
|
70
|
-
if DEV_MODE:
|
|
71
|
-
_LOGGER.setLevel(logging.DEBUG)
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
#
|
|
75
80
|
# 0/5: Schema strings
|
|
76
|
-
SZ_SCHEMA = "schema"
|
|
77
|
-
SZ_MAIN_TCS = "main_tcs"
|
|
81
|
+
SZ_SCHEMA: Final = "schema"
|
|
82
|
+
SZ_MAIN_TCS: Final = "main_tcs"
|
|
78
83
|
|
|
79
|
-
SZ_CONTROLLER = DEV_TYPE_MAP[
|
|
80
|
-
SZ_SYSTEM = "system"
|
|
81
|
-
SZ_APPLIANCE_CONTROL = DEV_ROLE_MAP[
|
|
82
|
-
SZ_ORPHANS = "orphans"
|
|
83
|
-
SZ_ORPHANS_HEAT = "orphans_heat"
|
|
84
|
-
SZ_ORPHANS_HVAC = "orphans_hvac"
|
|
84
|
+
SZ_CONTROLLER = DEV_TYPE_MAP[DevType.CTL]
|
|
85
|
+
SZ_SYSTEM: Final = "system"
|
|
86
|
+
SZ_APPLIANCE_CONTROL = DEV_ROLE_MAP[DevRole.APP]
|
|
87
|
+
SZ_ORPHANS: Final = "orphans"
|
|
88
|
+
SZ_ORPHANS_HEAT: Final = "orphans_heat"
|
|
89
|
+
SZ_ORPHANS_HVAC: Final = "orphans_hvac"
|
|
85
90
|
|
|
86
|
-
SZ_DHW_SYSTEM = "stored_hotwater"
|
|
87
|
-
SZ_DHW_SENSOR = DEV_ROLE_MAP[
|
|
88
|
-
SZ_DHW_VALVE = DEV_ROLE_MAP[
|
|
89
|
-
SZ_HTG_VALVE = DEV_ROLE_MAP[
|
|
91
|
+
SZ_DHW_SYSTEM: Final = "stored_hotwater"
|
|
92
|
+
SZ_DHW_SENSOR = DEV_ROLE_MAP[DevRole.DHW]
|
|
93
|
+
SZ_DHW_VALVE = DEV_ROLE_MAP[DevRole.HTG]
|
|
94
|
+
SZ_HTG_VALVE = DEV_ROLE_MAP[DevRole.HT1]
|
|
90
95
|
|
|
91
|
-
SZ_SENSOR_FAKED = "sensor_faked"
|
|
96
|
+
SZ_SENSOR_FAKED: Final = "sensor_faked"
|
|
92
97
|
|
|
93
|
-
SZ_UFH_SYSTEM = "underfloor_heating"
|
|
94
|
-
SZ_UFH_CTL = DEV_TYPE_MAP[
|
|
95
|
-
SZ_CIRCUITS = "circuits"
|
|
98
|
+
SZ_UFH_SYSTEM: Final = "underfloor_heating"
|
|
99
|
+
SZ_UFH_CTL = DEV_TYPE_MAP[DevType.UFC] # ufh_controller
|
|
100
|
+
SZ_CIRCUITS: Final = "circuits"
|
|
96
101
|
|
|
97
102
|
HEAT_ZONES_STRS = tuple(ZON_ROLE_MAP[t] for t in ZON_ROLE_MAP.HEAT_ZONES)
|
|
98
103
|
|
|
@@ -109,7 +114,7 @@ def ErrorRenamedKey(new_key: str) -> Callable[[Any], None]:
|
|
|
109
114
|
|
|
110
115
|
|
|
111
116
|
#
|
|
112
|
-
# 1/
|
|
117
|
+
# 1/7: Schemas for CH/DHW systems, aka Heat/TCS (temp control systems)
|
|
113
118
|
SCH_TCS_SYS_CLASS = (SystemType.EVOHOME, SystemType.HOMETRONICS, SystemType.SUNDIAL)
|
|
114
119
|
SCH_TCS_SYS = vol.Schema(
|
|
115
120
|
{
|
|
@@ -117,7 +122,6 @@ SCH_TCS_SYS = vol.Schema(
|
|
|
117
122
|
None, SCH_DEVICE_ID_APP
|
|
118
123
|
),
|
|
119
124
|
vol.Optional("heating_control"): ErrorRenamedKey(SZ_APPLIANCE_CONTROL),
|
|
120
|
-
# vol.Optional(SZ_CLASS, default=SystemType.EVOHOME): vol.Any(*SCH_TCS_SYS_CLASS),
|
|
121
125
|
},
|
|
122
126
|
extra=vol.PREVENT_EXTRA,
|
|
123
127
|
)
|
|
@@ -134,8 +138,8 @@ SCH_TCS_DHW = vol.Schema(
|
|
|
134
138
|
|
|
135
139
|
_CH_TCS_UFH_CIRCUIT = vol.Schema(
|
|
136
140
|
{
|
|
137
|
-
vol.Required(SCH_UFH_IDX): vol.
|
|
138
|
-
{vol.Optional(SZ_ZONE_IDX):
|
|
141
|
+
vol.Required(SCH_UFH_IDX): vol.Schema(
|
|
142
|
+
{vol.Optional(SZ_ZONE_IDX): SCH_ZON_IDX},
|
|
139
143
|
),
|
|
140
144
|
},
|
|
141
145
|
extra=vol.PREVENT_EXTRA,
|
|
@@ -163,7 +167,7 @@ SCH_TCS_ZONES_ZON = vol.Schema(
|
|
|
163
167
|
vol.Optional(SZ_ZONE_TYPE): ErrorRenamedKey(SZ_CLASS),
|
|
164
168
|
vol.Optional("zone_sensor"): ErrorRenamedKey(SZ_SENSOR),
|
|
165
169
|
# vol.Optional(SZ_SENSOR_FAKED): bool,
|
|
166
|
-
vol.Optional(f"_{SZ_NAME}"): vol.Any(
|
|
170
|
+
vol.Optional(f"_{SZ_NAME}"): vol.Any(None, str),
|
|
167
171
|
},
|
|
168
172
|
extra=vol.PREVENT_EXTRA,
|
|
169
173
|
)
|
|
@@ -178,8 +182,8 @@ SCH_TCS = vol.Schema(
|
|
|
178
182
|
vol.Optional(SZ_SYSTEM, default={}): vol.Any({}, SCH_TCS_SYS),
|
|
179
183
|
vol.Optional(SZ_DHW_SYSTEM, default={}): vol.Any({}, SCH_TCS_DHW),
|
|
180
184
|
vol.Optional(SZ_UFH_SYSTEM, default={}): vol.Any({}, SCH_TCS_UFH),
|
|
181
|
-
vol.Optional(SZ_ORPHANS, default=[]): vol.
|
|
182
|
-
[], vol.Unique(
|
|
185
|
+
vol.Optional(SZ_ORPHANS, default=[]): vol.All(
|
|
186
|
+
[SCH_DEVICE_ID_ANY], vol.Unique()
|
|
183
187
|
),
|
|
184
188
|
vol.Optional(SZ_ZONES, default={}): vol.Any({}, SCH_TCS_ZONES),
|
|
185
189
|
vol.Optional(vol.Remove("is_tcs")): vol.Coerce(bool),
|
|
@@ -189,17 +193,19 @@ SCH_TCS = vol.Schema(
|
|
|
189
193
|
|
|
190
194
|
|
|
191
195
|
#
|
|
192
|
-
# 2/
|
|
193
|
-
SZ_REMOTES = "remotes"
|
|
194
|
-
SZ_SENSORS = "sensors"
|
|
196
|
+
# 2/7: Schemas for Ventilation control systems, aka HVAC/VCS
|
|
197
|
+
SZ_REMOTES: Final = "remotes"
|
|
198
|
+
SZ_SENSORS: Final = "sensors"
|
|
195
199
|
|
|
196
200
|
SCH_VCS_DATA = vol.Schema(
|
|
197
201
|
{
|
|
198
|
-
vol.Optional(SZ_REMOTES, default=[]): vol.
|
|
199
|
-
[],
|
|
202
|
+
vol.Optional(SZ_REMOTES, default=[]): vol.All(
|
|
203
|
+
[SCH_DEVICE_ID_ANY],
|
|
204
|
+
vol.Unique(), # vol.Length(min=1)
|
|
200
205
|
),
|
|
201
|
-
vol.Optional(SZ_SENSORS, default=[]): vol.
|
|
202
|
-
[],
|
|
206
|
+
vol.Optional(SZ_SENSORS, default=[]): vol.All(
|
|
207
|
+
[SCH_DEVICE_ID_ANY],
|
|
208
|
+
vol.Unique(), # vol.Length(min=1)
|
|
203
209
|
),
|
|
204
210
|
vol.Optional(vol.Remove("is_vcs")): vol.Coerce(bool),
|
|
205
211
|
},
|
|
@@ -221,37 +227,33 @@ SCH_VCS = vol.All(SCH_VCS_KEYS, SCH_VCS_DATA)
|
|
|
221
227
|
|
|
222
228
|
|
|
223
229
|
#
|
|
224
|
-
# 3/
|
|
230
|
+
# 3/7: Global Schema for Heat/HVAC systems
|
|
225
231
|
SCH_GLOBAL_SCHEMAS_DICT = { # System schemas - can be 0-many Heat/HVAC schemas
|
|
226
|
-
# orphans are devices to create that
|
|
232
|
+
# orphans are devices to create that won't be in a (cached) schema...
|
|
227
233
|
vol.Optional(SZ_MAIN_TCS): vol.Any(None, SCH_DEVICE_ID_CTL),
|
|
228
234
|
vol.Optional(vol.Remove("main_controller")): vol.Any(None, SCH_DEVICE_ID_CTL),
|
|
229
235
|
vol.Optional(SCH_DEVICE_ID_CTL): vol.Any(SCH_TCS, SCH_VCS),
|
|
230
236
|
vol.Optional(SCH_DEVICE_ID_ANY): SCH_VCS, # must be after SCH_DEVICE_ID_CTL
|
|
231
|
-
vol.Optional(SZ_ORPHANS_HEAT): vol.All(
|
|
232
|
-
|
|
233
|
-
),
|
|
234
|
-
vol.Optional(SZ_ORPHANS_HVAC): vol.All(
|
|
235
|
-
vol.Unique([SCH_DEVICE_ID_ANY]), vol.Length(min=0)
|
|
236
|
-
),
|
|
237
|
+
vol.Optional(SZ_ORPHANS_HEAT): vol.All([SCH_DEVICE_ID_ANY], vol.Unique()),
|
|
238
|
+
vol.Optional(SZ_ORPHANS_HVAC): vol.All([SCH_DEVICE_ID_ANY], vol.Unique()),
|
|
237
239
|
}
|
|
238
|
-
|
|
240
|
+
SCH_GLOBAL_SCHEMAS = vol.Schema(SCH_GLOBAL_SCHEMAS_DICT, extra=vol.PREVENT_EXTRA)
|
|
239
241
|
|
|
240
242
|
#
|
|
241
|
-
# 4/
|
|
242
|
-
SZ_DISABLE_DISCOVERY = "disable_discovery"
|
|
243
|
-
SZ_ENABLE_EAVESDROP = "enable_eavesdrop"
|
|
244
|
-
SZ_MAX_ZONES = "max_zones" # TODO: move to TCS-attr from GWY-layer
|
|
245
|
-
SZ_REDUCE_PROCESSING = "reduce_processing"
|
|
246
|
-
SZ_USE_ALIASES = "use_aliases" # use friendly device names from known_list
|
|
247
|
-
SZ_USE_NATIVE_OT = "use_native_ot" # favour OT (3220s) over RAMSES
|
|
248
|
-
|
|
249
|
-
SCH_GATEWAY_DICT =
|
|
243
|
+
# 4/7: Gateway (parser/state) configuration
|
|
244
|
+
SZ_DISABLE_DISCOVERY: Final = "disable_discovery"
|
|
245
|
+
SZ_ENABLE_EAVESDROP: Final = "enable_eavesdrop"
|
|
246
|
+
SZ_MAX_ZONES: Final = "max_zones" # TODO: move to TCS-attr from GWY-layer
|
|
247
|
+
SZ_REDUCE_PROCESSING: Final = "reduce_processing"
|
|
248
|
+
SZ_USE_ALIASES: Final = "use_aliases" # use friendly device names from known_list
|
|
249
|
+
SZ_USE_NATIVE_OT: Final = "use_native_ot" # favour OT (3220s) over RAMSES
|
|
250
|
+
|
|
251
|
+
SCH_GATEWAY_DICT = {
|
|
250
252
|
vol.Optional(SZ_DISABLE_DISCOVERY, default=False): bool,
|
|
251
253
|
vol.Optional(SZ_ENABLE_EAVESDROP, default=False): bool,
|
|
252
254
|
vol.Optional(SZ_MAX_ZONES, default=DEFAULT_MAX_ZONES): vol.All(
|
|
253
255
|
int, vol.Range(min=1, max=16)
|
|
254
|
-
), #
|
|
256
|
+
), # NOTE: no default
|
|
255
257
|
vol.Optional(SZ_REDUCE_PROCESSING, default=0): vol.All(
|
|
256
258
|
int, vol.Range(min=0, max=DONT_CREATE_MESSAGES)
|
|
257
259
|
),
|
|
@@ -260,40 +262,46 @@ SCH_GATEWAY_DICT = SCH_ENGINE_DICT | {
|
|
|
260
262
|
"always", "prefer", "avoid", "never"
|
|
261
263
|
),
|
|
262
264
|
}
|
|
265
|
+
SCH_GATEWAY_CONFIG = vol.Schema(SCH_GATEWAY_DICT, extra=vol.REMOVE_EXTRA)
|
|
263
266
|
|
|
264
267
|
|
|
265
268
|
#
|
|
266
|
-
# 5/
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
SCH_GLOBAL_CONFIG = vol.All(
|
|
269
|
+
# 5/7: the Global (gateway) Schema
|
|
270
|
+
SCH_GLOBAL_CONFIG = (
|
|
270
271
|
vol.Schema(
|
|
271
272
|
{
|
|
272
|
-
# Gateway/engine
|
|
273
|
-
vol.Optional(SZ_CONFIG, default={}): SCH_GATEWAY_DICT
|
|
273
|
+
# Gateway/engine Configuration, incl. packet_log, serial_port params...
|
|
274
|
+
vol.Optional(SZ_CONFIG, default={}): SCH_GATEWAY_DICT | SCH_ENGINE_DICT
|
|
274
275
|
},
|
|
275
276
|
extra=vol.PREVENT_EXTRA,
|
|
276
277
|
)
|
|
277
278
|
.extend(SCH_GLOBAL_SCHEMAS_DICT)
|
|
278
279
|
.extend(SCH_GLOBAL_TRAITS_DICT)
|
|
279
|
-
.extend(sch_packet_log_dict_factory(default_backups=0))
|
|
280
|
+
.extend(sch_packet_log_dict_factory(default_backups=0))
|
|
280
281
|
)
|
|
281
282
|
|
|
282
283
|
|
|
283
284
|
#
|
|
284
|
-
# 6/
|
|
285
|
-
def NormaliseRestoreCache():
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
# 6/7: External Schemas, to be used by clients of this library
|
|
286
|
+
def NormaliseRestoreCache() -> Callable[[bool | dict[str, bool]], dict[str, bool]]:
|
|
287
|
+
"""Convert a short-hand restore_cache bool to a dict.
|
|
288
|
+
|
|
289
|
+
restore_cache: bool -> restore_cache:
|
|
290
|
+
restore_schema: bool
|
|
291
|
+
restore_state: bool
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def normalise_restore_cache(node_value: bool | dict[str, bool]) -> dict[str, bool]:
|
|
295
|
+
if isinstance(node_value, dict):
|
|
288
296
|
return node_value
|
|
289
297
|
return {SZ_RESTORE_SCHEMA: node_value, SZ_RESTORE_STATE: node_value}
|
|
290
298
|
|
|
291
299
|
return normalise_restore_cache
|
|
292
300
|
|
|
293
301
|
|
|
294
|
-
SZ_RESTORE_CACHE = "restore_cache"
|
|
295
|
-
SZ_RESTORE_SCHEMA = "restore_schema"
|
|
296
|
-
SZ_RESTORE_STATE = "restore_state"
|
|
302
|
+
SZ_RESTORE_CACHE: Final = "restore_cache"
|
|
303
|
+
SZ_RESTORE_SCHEMA: Final = "restore_schema"
|
|
304
|
+
SZ_RESTORE_STATE: Final = "restore_state"
|
|
297
305
|
|
|
298
306
|
SCH_RESTORE_CACHE_DICT = {
|
|
299
307
|
vol.Optional(SZ_RESTORE_CACHE, default=True): vol.Any(
|
|
@@ -309,91 +317,20 @@ SCH_RESTORE_CACHE_DICT = {
|
|
|
309
317
|
|
|
310
318
|
|
|
311
319
|
#
|
|
312
|
-
#
|
|
313
|
-
def
|
|
314
|
-
"""
|
|
315
|
-
return {
|
|
316
|
-
k: v
|
|
317
|
-
for k, v in kwargs.items()
|
|
318
|
-
if DEVICE_ID_REGEX.ANY.match(k)
|
|
319
|
-
or k in (SZ_MAIN_TCS, SZ_ORPHANS_HEAT, SZ_ORPHANS_HVAC)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
# def extract_config(**kwargs) -> dict:
|
|
323
|
-
# """Return the config embedded with a global configuration."""
|
|
324
|
-
# return {
|
|
325
|
-
# k: v
|
|
326
|
-
# for k, v in kwargs.items()
|
|
327
|
-
# if not DEVICE_ID_REGEX.ANY.match(k) and k not in (
|
|
328
|
-
# SZ_MAIN_TCS, SZ_ORPHANS_HEAT, SZ_ORPHANS_HVAC
|
|
329
|
-
# )
|
|
330
|
-
# }
|
|
331
|
-
|
|
332
|
-
# def split_configuration(**kwargs) -> tuple[dict, dict]:
|
|
333
|
-
# """Split a global configuration into non-schema (config) & schema."""
|
|
334
|
-
# return extract_config(**kwargs), extract_schema(**kwargs),
|
|
335
|
-
|
|
336
|
-
pass
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def load_config(
|
|
340
|
-
port_name: None | str,
|
|
341
|
-
input_file: TextIO,
|
|
342
|
-
config: dict[str, Any] = None,
|
|
343
|
-
packet_log: None | dict[str, Any] = None,
|
|
344
|
-
block_list: dict[_DeviceIdT, dict] = None,
|
|
345
|
-
known_list: dict[_DeviceIdT, dict] = None,
|
|
346
|
-
**schema,
|
|
347
|
-
) -> tuple[SimpleNamespace, dict, dict, dict]:
|
|
348
|
-
"""Process the configuration, including any filter lists.
|
|
349
|
-
|
|
350
|
-
Returns:
|
|
351
|
-
- config (includes config.enforce_known_list)
|
|
352
|
-
- schema (processed further later on)
|
|
353
|
-
- known_list (is a dict)
|
|
354
|
-
- block_list (is a dict)
|
|
355
|
-
"""
|
|
356
|
-
|
|
357
|
-
if port_name and input_file:
|
|
358
|
-
_LOGGER.warning(
|
|
359
|
-
"Serial port was specified (%s), so input file (%s) will be ignored",
|
|
360
|
-
port_name,
|
|
361
|
-
input_file,
|
|
362
|
-
)
|
|
363
|
-
elif port_name is None:
|
|
364
|
-
config[SZ_DISABLE_SENDING] = True
|
|
365
|
-
|
|
366
|
-
if config[SZ_DISABLE_SENDING]:
|
|
367
|
-
config[SZ_DISABLE_DISCOVERY] = True
|
|
368
|
-
|
|
369
|
-
if config[SZ_ENABLE_EAVESDROP]:
|
|
370
|
-
_LOGGER.warning(
|
|
371
|
-
f"{SZ_ENABLE_EAVESDROP} enabled: this is strongly discouraged"
|
|
372
|
-
" for routine use (there be dragons here)"
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
config[SZ_ENFORCE_KNOWN_LIST] = select_device_filter_mode(
|
|
376
|
-
config[SZ_ENFORCE_KNOWN_LIST], known_list, block_list
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
config[SZ_PACKET_LOG] = packet_log
|
|
380
|
-
|
|
381
|
-
# assert schema == extract_schema(**schema)
|
|
382
|
-
|
|
383
|
-
return (SimpleNamespace(**config), schema, known_list, block_list)
|
|
320
|
+
# 7/7: Other stuff
|
|
321
|
+
def _get_device(gwy: Gateway, dev_id: DeviceIdT, **kwargs: Any) -> Device: # , **traits
|
|
322
|
+
"""Get a device from the gateway.
|
|
384
323
|
|
|
385
|
-
|
|
386
|
-
def _get_device(gwy, dev_id: str, **kwargs) -> Any: # Device
|
|
387
|
-
"""Raise an LookupError if a device_id is filtered out by a list.
|
|
324
|
+
Raise a LookupError if a device_id is filtered out by a list.
|
|
388
325
|
|
|
389
326
|
The underlying method is wrapped only to provide a better error message.
|
|
390
327
|
"""
|
|
391
328
|
|
|
392
|
-
def check_filter_lists(dev_id:
|
|
329
|
+
def check_filter_lists(dev_id: DeviceIdT) -> None:
|
|
393
330
|
"""Raise an LookupError if a device_id is filtered out by a list."""
|
|
394
331
|
|
|
395
332
|
err_msg = None
|
|
396
|
-
if gwy.
|
|
333
|
+
if gwy._enforce_known_list and dev_id not in gwy._include:
|
|
397
334
|
err_msg = f"it is in the {SZ_SCHEMA}, but not in the {SZ_KNOWN_LIST}"
|
|
398
335
|
if dev_id in gwy._exclude:
|
|
399
336
|
err_msg = f"it is in the {SZ_SCHEMA}, but also in the {SZ_BLOCK_LIST}"
|
|
@@ -408,29 +345,46 @@ def _get_device(gwy, dev_id: str, **kwargs) -> Any: # Device
|
|
|
408
345
|
return gwy.get_device(dev_id, **kwargs)
|
|
409
346
|
|
|
410
347
|
|
|
411
|
-
def load_schema(
|
|
412
|
-
|
|
348
|
+
def load_schema(
|
|
349
|
+
gwy: Gateway, known_list: dict[DeviceIdT, Any] | None = None, **schema: Any
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Instantiate all entities in the schema, and faked devices in the known_list."""
|
|
352
|
+
|
|
353
|
+
from .device import Fakeable # circular import
|
|
354
|
+
|
|
355
|
+
known_list = known_list or {}
|
|
356
|
+
|
|
357
|
+
# schema: dict = SCH_GLOBAL_SCHEMAS_DICT(schema)
|
|
413
358
|
|
|
414
359
|
[
|
|
415
|
-
load_tcs(gwy, ctl_id, schema)
|
|
416
|
-
for ctl_id, schema in
|
|
360
|
+
load_tcs(gwy, ctl_id, schema) # type: ignore[arg-type]
|
|
361
|
+
for ctl_id, schema in schema.items()
|
|
417
362
|
if re.match(DEVICE_ID_REGEX.ANY, ctl_id) and SZ_REMOTES not in schema
|
|
418
363
|
]
|
|
419
|
-
if
|
|
420
|
-
gwy._tcs = gwy.system_by_id.get(
|
|
364
|
+
if schema.get(SZ_MAIN_TCS):
|
|
365
|
+
gwy._tcs = gwy.system_by_id.get(schema[SZ_MAIN_TCS])
|
|
421
366
|
[
|
|
422
|
-
load_fan(gwy, fan_id, schema)
|
|
423
|
-
for fan_id, schema in
|
|
367
|
+
load_fan(gwy, fan_id, schema) # type: ignore[arg-type]
|
|
368
|
+
for fan_id, schema in schema.items()
|
|
424
369
|
if re.match(DEVICE_ID_REGEX.ANY, fan_id) and SZ_REMOTES in schema
|
|
425
370
|
]
|
|
426
371
|
[ # NOTE: class favoured, domain ignored
|
|
427
372
|
_get_device(gwy, device_id) # domain=key[-4:])
|
|
428
373
|
for key in (SZ_ORPHANS_HEAT, SZ_ORPHANS_HVAC)
|
|
429
|
-
for device_id in
|
|
374
|
+
for device_id in schema.get(key, [])
|
|
430
375
|
] # TODO: pass domain (Heat/HVAC), or generalise to SZ_ORPHANS
|
|
431
376
|
|
|
377
|
+
# create any devices in the known list that are faked, or fake those already created
|
|
378
|
+
for device_id, traits in known_list.items():
|
|
379
|
+
if traits.get(SZ_FAKED):
|
|
380
|
+
dev = _get_device(gwy, device_id) # , **traits)
|
|
381
|
+
if not isinstance(dev, Fakeable):
|
|
382
|
+
raise exc.SystemSchemaInconsistent(f"Device is not fakeable: {dev}")
|
|
383
|
+
if not dev.is_faked:
|
|
384
|
+
dev._make_fake()
|
|
385
|
+
|
|
432
386
|
|
|
433
|
-
def load_fan(gwy, fan_id:
|
|
387
|
+
def load_fan(gwy: Gateway, fan_id: DeviceIdT, schema: dict[str, Any]) -> Device:
|
|
434
388
|
"""Create a FAN using its schema (i.e. with remotes, sensors)."""
|
|
435
389
|
|
|
436
390
|
fan = _get_device(gwy, fan_id)
|
|
@@ -439,27 +393,27 @@ def load_fan(gwy, fan_id: str, schema: dict) -> Any: # Device
|
|
|
439
393
|
return fan
|
|
440
394
|
|
|
441
395
|
|
|
442
|
-
def load_tcs(gwy, ctl_id:
|
|
396
|
+
def load_tcs(gwy: Gateway, ctl_id: DeviceIdT, schema: dict[str, Any]) -> Evohome:
|
|
443
397
|
"""Create a TCS using its schema."""
|
|
444
398
|
# print(schema)
|
|
445
399
|
# schema = SCH_TCS_ZONES_ZON(schema)
|
|
446
400
|
|
|
447
401
|
ctl = _get_device(gwy, ctl_id)
|
|
448
|
-
ctl.tcs._update_schema(**schema)
|
|
402
|
+
ctl.tcs._update_schema(**schema)
|
|
449
403
|
|
|
450
|
-
for dev_id in schema.get(SZ_UFH_SYSTEM, {})
|
|
404
|
+
for dev_id in schema.get(SZ_UFH_SYSTEM, {}): # UFH controllers
|
|
451
405
|
_get_device(gwy, dev_id, parent=ctl.tcs) # , **_schema)
|
|
452
406
|
|
|
453
407
|
for dev_id in schema.get(SZ_ORPHANS, []):
|
|
454
408
|
_get_device(gwy, dev_id, parent=ctl)
|
|
455
409
|
|
|
456
|
-
if
|
|
457
|
-
|
|
410
|
+
# if DEV_MODE:
|
|
411
|
+
# import json
|
|
458
412
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
413
|
+
# src = json.dumps(shrink(schema), sort_keys=True)
|
|
414
|
+
# dst = json.dumps(shrink(gwy.system_by_id[ctl.id].schema), sort_keys=True)
|
|
415
|
+
# # assert dst == src, "They don't match!"
|
|
416
|
+
# print(src)
|
|
417
|
+
# print(dst)
|
|
464
418
|
|
|
465
419
|
return ctl.tcs
|