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.
Files changed (71) hide show
  1. ramses_cli/__init__.py +18 -0
  2. ramses_cli/client.py +597 -0
  3. ramses_cli/debug.py +20 -0
  4. ramses_cli/discovery.py +405 -0
  5. ramses_cli/utils/cat_slow.py +17 -0
  6. ramses_cli/utils/convert.py +60 -0
  7. ramses_rf/__init__.py +31 -10
  8. ramses_rf/binding_fsm.py +787 -0
  9. ramses_rf/const.py +124 -105
  10. ramses_rf/database.py +297 -0
  11. ramses_rf/device/__init__.py +69 -39
  12. ramses_rf/device/base.py +187 -376
  13. ramses_rf/device/heat.py +540 -552
  14. ramses_rf/device/hvac.py +286 -171
  15. ramses_rf/dispatcher.py +153 -177
  16. ramses_rf/entity_base.py +478 -361
  17. ramses_rf/exceptions.py +82 -0
  18. ramses_rf/gateway.py +377 -513
  19. ramses_rf/helpers.py +57 -19
  20. ramses_rf/py.typed +0 -0
  21. ramses_rf/schemas.py +148 -194
  22. ramses_rf/system/__init__.py +16 -23
  23. ramses_rf/system/faultlog.py +363 -0
  24. ramses_rf/system/heat.py +295 -302
  25. ramses_rf/system/schedule.py +312 -198
  26. ramses_rf/system/zones.py +318 -238
  27. ramses_rf/version.py +2 -8
  28. ramses_rf-0.51.1.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.1.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
  33. ramses_tx/__init__.py +160 -0
  34. {ramses_rf/protocol → ramses_tx}/address.py +65 -59
  35. ramses_tx/command.py +1454 -0
  36. ramses_tx/const.py +903 -0
  37. ramses_tx/exceptions.py +92 -0
  38. {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
  39. {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
  40. ramses_tx/gateway.py +338 -0
  41. ramses_tx/helpers.py +883 -0
  42. {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
  43. {ramses_rf/protocol → ramses_tx}/message.py +155 -191
  44. ramses_tx/opentherm.py +1260 -0
  45. ramses_tx/packet.py +210 -0
  46. {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  47. ramses_tx/protocol.py +801 -0
  48. ramses_tx/protocol_fsm.py +672 -0
  49. ramses_tx/py.typed +0 -0
  50. {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
  51. {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
  52. ramses_tx/transport.py +1471 -0
  53. ramses_tx/typed_dicts.py +492 -0
  54. ramses_tx/typing.py +181 -0
  55. ramses_tx/version.py +4 -0
  56. ramses_rf/discovery.py +0 -398
  57. ramses_rf/protocol/__init__.py +0 -59
  58. ramses_rf/protocol/backports.py +0 -42
  59. ramses_rf/protocol/command.py +0 -1576
  60. ramses_rf/protocol/const.py +0 -697
  61. ramses_rf/protocol/exceptions.py +0 -111
  62. ramses_rf/protocol/helpers.py +0 -390
  63. ramses_rf/protocol/opentherm.py +0 -1170
  64. ramses_rf/protocol/packet.py +0 -235
  65. ramses_rf/protocol/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. 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
- def merge(src: dict, dst: dict, _dc: bool = None) -> dict: # TODO: move to ramses_rf?
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
- merge(value, node, _dc=True)
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 precidence, assert will fail
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(value: dict, keep_falsys: bool = False, keep_hints: bool = False) -> dict:
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
- return walk(value)
92
+ result: _SchemaT = walk(value)
93
+ return result
70
94
 
71
95
 
72
- def schedule_task(fnc, *args, delay=None, period=None, **kwargs) -> asyncio.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 execute_func(fnc, *args, **kwargs):
76
- if iscoroutinefunction(fnc):
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 schedule_func(delay, period, fnc, *args, **kwargs):
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
- asyncio.create_task(execute_func(fnc, *args, **kwargs), name=str(fnc))
123
+ await execute_fnc(fnc, *args, **kwargs)
86
124
  return
87
125
 
88
126
  while period:
89
- asyncio.create_task(execute_func(fnc, *args, **kwargs), name=str(fnc))
127
+ await execute_fnc(fnc, *args, **kwargs)
90
128
  await asyncio.sleep(period)
91
129
 
92
- return asyncio.create_task(
93
- schedule_func(delay, period, fnc, *args, **kwargs), name=str(fnc)
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 types import SimpleNamespace
13
- from typing import Any, Callable, TextIO
11
+ from collections.abc import Callable
12
+ from typing import TYPE_CHECKING, Any, Final
14
13
 
15
- import voluptuous as vol # type: ignore[import]
14
+ import voluptuous as vol
16
15
 
17
- from .const import (
18
- DEFAULT_MAX_ZONES,
19
- DEV_ROLE,
20
- DEV_ROLE_MAP,
21
- DEV_TYPE,
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
- from .protocol.frame import _DeviceIdT
40
- from .protocol.schemas import ( # noqa: F401
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
- # from .systems import _SystemT # circular import
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[DEV_TYPE.CTL]
80
- SZ_SYSTEM = "system"
81
- SZ_APPLIANCE_CONTROL = DEV_ROLE_MAP[DEV_ROLE.APP]
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[DEV_ROLE.DHW]
88
- SZ_DHW_VALVE = DEV_ROLE_MAP[DEV_ROLE.HTG]
89
- SZ_HTG_VALVE = DEV_ROLE_MAP[DEV_ROLE.HT1]
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[DEV_TYPE.UFC] # ufh_controller
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/5: Schemas for CH/DHW systems, aka Heat/TCS (temp control systems)
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.Any(
138
- {vol.Optional(SZ_ZONE_IDX): vol.Any(SCH_ZON_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(str, None),
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.Any(
182
- [], vol.Unique([SCH_DEVICE_ID_ANY])
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/5: Schemas for Ventilation control systems, aka HVAC/VCS
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.Any(
199
- [], vol.Unique([SCH_DEVICE_ID_ANY])
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.Any(
202
- [], vol.Unique([SCH_DEVICE_ID_ANY])
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/5: Global Schema for Heat/HVAC systems
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 wont be in a (cached) schema...
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
- vol.Unique([SCH_DEVICE_ID_ANY]), vol.Length(min=0)
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/5: Gateway (parser/state) configuration
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 = SCH_ENGINE_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
- ), # TODO: no default
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/5: the Global (gateway) Schema
267
- SZ_CONFIG = "config"
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 Configuraton, incl. packet_log, serial_port params...
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/5: External Schemas, to be used by clients of this library
285
- def NormaliseRestoreCache():
286
- def normalise_restore_cache(node_value) -> None:
287
- if not isinstance(node_value, bool):
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
- # 6/5: Other stuff
313
- def extract_schema(**kwargs) -> dict:
314
- """Return the schema embedded with a global configuration."""
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: str) -> None:
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.config.enforce_known_list and dev_id not in gwy._include:
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(gwy, **kwargs) -> None:
412
- """Process the schema, and the configuration and return True if it is valid."""
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 kwargs.items()
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 kwargs.get(SZ_MAIN_TCS):
420
- gwy._tcs = gwy.system_by_id.get(kwargs[SZ_MAIN_TCS])
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 kwargs.items()
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 kwargs.get(key, [])
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: str, schema: dict) -> Any: # Device
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: str, schema: dict) -> Any: # System
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) # TODO
402
+ ctl.tcs._update_schema(**schema)
449
403
 
450
- for dev_id in schema.get(SZ_UFH_SYSTEM, {}).keys(): # UFH controllers
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 False and DEV_MODE:
457
- import json
410
+ # if DEV_MODE:
411
+ # import json
458
412
 
459
- src = json.dumps(shrink(schema), sort_keys=True)
460
- dst = json.dumps(shrink(gwy.system_by_id[ctl.id].schema), sort_keys=True)
461
- # assert dst == src, "They don't match!"
462
- print(src)
463
- print(dst)
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