ramses-rf 0.22.2__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 (72) 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 +378 -514
  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.2.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.2.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_tx/parsers.py +2957 -0
  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 -1561
  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/parsers.py +0 -2673
  66. ramses_rf/protocol/protocol.py +0 -613
  67. ramses_rf/protocol/transport.py +0 -1011
  68. ramses_rf/protocol/version.py +0 -10
  69. ramses_rf/system/hvac.py +0 -82
  70. ramses_rf-0.22.2.dist-info/METADATA +0 -64
  71. ramses_rf-0.22.2.dist-info/RECORD +0 -42
  72. ramses_rf-0.22.2.dist-info/top_level.txt +0 -1
@@ -1,89 +1,78 @@
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 protocol (lower) layer.
7
5
  """
6
+
8
7
  from __future__ import annotations
9
8
 
10
9
  import logging
11
- from typing import TextIO
12
-
13
- import voluptuous as vol # type: ignore[import]
14
-
15
- from .const import DEV_TYPE, DEV_TYPE_MAP, DEVICE_ID_REGEX, __dev_mode__
16
-
17
- DEV_MODE = __dev_mode__ and False
10
+ from collections.abc import Callable
11
+ from typing import Any, Final, Never, NewType, TypeAlias, TypedDict, TypeVar
12
+
13
+ import voluptuous as vol
14
+
15
+ from .const import (
16
+ DEFAULT_ECHO_TIMEOUT,
17
+ DEFAULT_RPLY_TIMEOUT,
18
+ DEV_TYPE_MAP,
19
+ DEVICE_ID_REGEX,
20
+ MAX_DUTY_CYCLE_RATE,
21
+ MIN_INTER_WRITE_GAP,
22
+ )
18
23
 
19
24
  _LOGGER = logging.getLogger(__name__)
20
- if DEV_MODE:
21
- _LOGGER.setLevel(logging.DEBUG)
22
25
 
23
26
 
24
27
  #
25
28
  # 0/5: Packet source configuration
26
- SZ_INPUT_FILE = "input_file"
27
- SZ_PACKET_SOURCE = "packet_source"
28
-
29
-
30
- def WIP_sch_packet_source_dict_factory() -> dict[vol.Required, vol.Any]:
31
- """Return a packet source dict.
32
-
33
- usage:
34
-
35
- SCH_PACKET_SOURCE = vol.Schema(
36
- sch_packet_source_dict_factory(), extra=vol.PREVENT_EXTRA
37
- )
38
- """
39
-
40
- SCH_PACKET_SOURCE_CONFIG = vol.Schema(
41
- {},
42
- extra=vol.PREVENT_EXTRA,
43
- )
44
-
45
- SCH_PACKET_SOURCE_FILE = TextIO
46
-
47
- def NormalisePacketSource():
48
- def normalise_packet_source(node_value: str | dict) -> dict:
49
- if isinstance(node_value, str):
50
- return {
51
- SZ_INPUT_FILE: node_value,
52
- }
53
- return node_value
29
+ SZ_COMMS_PARAMS: Final = "comms_params"
30
+ SZ_DUTY_CYCLE_LIMIT: Final = "duty_cycle_limit"
31
+ SZ_GAP_BETWEEN_WRITES: Final = "gap_between_writes"
32
+ SZ_ECHO_TIMEOUT: Final = "echo_timeout"
33
+ SZ_RPLY_TIMEOUT: Final = "reply_timeout"
54
34
 
55
- return normalise_packet_source
35
+ SCH_COMMS_PARAMS = vol.Schema(
36
+ {
37
+ vol.Required(SZ_DUTY_CYCLE_LIMIT, default=MAX_DUTY_CYCLE_RATE): vol.All(
38
+ float, vol.Range(min=0.005, max=0.2)
39
+ ),
40
+ vol.Required(SZ_GAP_BETWEEN_WRITES, default=MIN_INTER_WRITE_GAP): vol.All(
41
+ float, vol.Range(min=0.05, max=1.0)
42
+ ),
43
+ vol.Required(SZ_ECHO_TIMEOUT, default=DEFAULT_ECHO_TIMEOUT): vol.All(
44
+ float, vol.Range(min=0.01, max=1.0)
45
+ ),
46
+ vol.Required(SZ_RPLY_TIMEOUT, default=DEFAULT_RPLY_TIMEOUT): vol.All(
47
+ float, vol.Range(min=0.01, max=1.0)
48
+ ),
49
+ },
50
+ extra=vol.PREVENT_EXTRA,
51
+ )
56
52
 
57
- return { # SCH_PACKET_LOG_DICT
58
- vol.Required(SZ_PACKET_LOG, default=None): vol.Any(
59
- None,
60
- vol.All(
61
- SCH_PACKET_SOURCE_FILE,
62
- NormalisePacketSource(),
63
- ),
64
- SCH_PACKET_SOURCE_CONFIG.extend(
65
- {vol.Required(SZ_INPUT_FILE): SCH_PACKET_SOURCE_FILE}
66
- ),
67
- )
68
- }
53
+ #
54
+ # 1/5: Packet source configuration
55
+ SZ_INPUT_FILE: Final = "input_file"
56
+ SZ_PACKET_SOURCE: Final = "packet_source"
69
57
 
70
58
 
71
- def extract_packet_source(pkt_source_dict: dict) -> tuple[str, dict]:
72
- """Extract a pkt source, source_config_dict tuple from a sch_packet_source_dict."""
73
- source_name = pkt_source_dict.get(SZ_INPUT_FILE)
74
- source_config = {k: v for k, v in pkt_source_dict.items() if k != SZ_INPUT_FILE}
75
- return source_name, source_config
59
+ #
60
+ # 2/5: Packet log configuration
61
+ SZ_FILE_NAME: Final = "file_name"
62
+ SZ_PACKET_LOG: Final = "packet_log"
63
+ SZ_ROTATE_BACKUPS: Final = "rotate_backups"
64
+ SZ_ROTATE_BYTES: Final = "rotate_bytes"
76
65
 
77
66
 
78
- #
79
- # 1/5: Packet log configuration
80
- SZ_FILE_NAME = "file_name"
81
- SZ_PACKET_LOG = "packet_log"
82
- SZ_ROTATE_BACKUPS = "rotate_backups"
83
- SZ_ROTATE_BYTES = "rotate_bytes"
67
+ class PktLogConfigT(TypedDict):
68
+ file_name: str
69
+ rotate_backups: int
70
+ rotate_bytes: int | None
84
71
 
85
72
 
86
- def sch_packet_log_dict_factory(default_backups=0) -> dict[vol.Required, vol.Any]:
73
+ def sch_packet_log_dict_factory(
74
+ default_backups: int = 0,
75
+ ) -> dict[vol.Required, vol.Any]:
87
76
  """Return a packet log dict with a configurable default rotation policy.
88
77
 
89
78
  usage:
@@ -105,8 +94,8 @@ def sch_packet_log_dict_factory(default_backups=0) -> dict[vol.Required, vol.Any
105
94
 
106
95
  SCH_PACKET_LOG_NAME = str
107
96
 
108
- def NormalisePacketLog(rotate_backups=0):
109
- def normalise_packet_log(node_value: str | dict) -> dict:
97
+ def NormalisePacketLog(rotate_backups: int = 0) -> Callable[..., Any]:
98
+ def normalise_packet_log(node_value: str | PktLogConfigT) -> PktLogConfigT:
110
99
  if isinstance(node_value, str):
111
100
  return {
112
101
  SZ_FILE_NAME: node_value,
@@ -131,17 +120,21 @@ def sch_packet_log_dict_factory(default_backups=0) -> dict[vol.Required, vol.Any
131
120
  }
132
121
 
133
122
 
123
+ SCH_PACKET_LOG = vol.Schema(
124
+ sch_packet_log_dict_factory(default_backups=7), extra=vol.PREVENT_EXTRA
125
+ )
126
+
134
127
  #
135
- # 2/5: Serial port configuration
136
- SZ_PORT_CONFIG = "port_config"
137
- SZ_PORT_NAME = "port_name"
138
- SZ_SERIAL_PORT = "serial_port"
128
+ # 3/5: Serial port configuration
129
+ SZ_PORT_CONFIG: Final = "port_config"
130
+ SZ_PORT_NAME: Final = "port_name"
131
+ SZ_SERIAL_PORT: Final = "serial_port"
139
132
 
140
- SZ_BAUDRATE = "baudrate"
141
- SZ_DSRDTR = "dsrdtr"
142
- SZ_RTSCTS = "rtscts"
143
- SZ_TIMEOUT = "timeout"
144
- SZ_XONXOFF = "xonxoff"
133
+ SZ_BAUDRATE: Final = "baudrate"
134
+ SZ_DSRDTR: Final = "dsrdtr"
135
+ SZ_RTSCTS: Final = "rtscts"
136
+ SZ_TIMEOUT: Final = "timeout"
137
+ SZ_XONXOFF: Final = "xonxoff"
145
138
 
146
139
 
147
140
  SCH_SERIAL_PORT_CONFIG = vol.Schema(
@@ -158,6 +151,14 @@ SCH_SERIAL_PORT_CONFIG = vol.Schema(
158
151
  )
159
152
 
160
153
 
154
+ class PortConfigT(TypedDict):
155
+ baudrate: int # 57600, 115200
156
+ dsrdtr: bool
157
+ rtscts: bool
158
+ timeout: int
159
+ xonxoff: bool
160
+
161
+
161
162
  def sch_serial_port_dict_factory() -> dict[vol.Required, vol.Any]:
162
163
  """Return a serial port dict.
163
164
 
@@ -170,10 +171,10 @@ def sch_serial_port_dict_factory() -> dict[vol.Required, vol.Any]:
170
171
 
171
172
  SCH_SERIAL_PORT_NAME = str
172
173
 
173
- def NormaliseSerialPort():
174
- def normalise_serial_port(node_value: str | dict) -> dict:
174
+ def NormaliseSerialPort() -> Callable[[str | PortConfigT], PortConfigT]:
175
+ def normalise_serial_port(node_value: str | PortConfigT) -> PortConfigT:
175
176
  if isinstance(node_value, str):
176
- return {SZ_PORT_NAME: node_value} | SCH_SERIAL_PORT_CONFIG({})
177
+ return {SZ_PORT_NAME: node_value} | SCH_SERIAL_PORT_CONFIG({}) # type: ignore[no-any-return]
177
178
  return node_value
178
179
 
179
180
  return normalise_serial_port
@@ -191,17 +192,21 @@ def sch_serial_port_dict_factory() -> dict[vol.Required, vol.Any]:
191
192
  }
192
193
 
193
194
 
194
- def extract_serial_port(ser_port_dict: dict) -> tuple[str, dict]:
195
+ def extract_serial_port(ser_port_dict: dict[str, Any]) -> tuple[str, PortConfigT]:
195
196
  """Extract a serial port, port_config_dict tuple from a sch_serial_port_dict."""
196
- port_name = ser_port_dict.get(SZ_PORT_NAME)
197
+ port_name: str = ser_port_dict.get(SZ_PORT_NAME) # type: ignore[assignment]
197
198
  port_config = {k: v for k, v in ser_port_dict.items() if k != SZ_PORT_NAME}
198
- return port_name, port_config
199
+ return port_name, port_config # type: ignore[return-value]
199
200
 
200
201
 
201
202
  #
202
- # 3/5: Traits (of devices) configuraion (basic) # TODO: moving from ..const
203
- def ConvertNullToDict():
204
- def convert_null_to_dict(node_value) -> dict:
203
+ # 4/5: Traits (of devices) configuration (basic)
204
+
205
+ _T = TypeVar("_T")
206
+
207
+
208
+ def ConvertNullToDict() -> Callable[[_T | None], _T | dict[Never, Never]]:
209
+ def convert_null_to_dict(node_value: _T | None) -> _T | dict[Never, Never]:
205
210
  if node_value is None:
206
211
  return {}
207
212
  return node_value
@@ -209,12 +214,13 @@ def ConvertNullToDict():
209
214
  return convert_null_to_dict
210
215
 
211
216
 
212
- SZ_ALIAS = "alias"
213
- SZ_CLASS = "class"
214
- SZ_FAKED = "faked"
217
+ SZ_ALIAS: Final = "alias"
218
+ SZ_CLASS: Final = "class"
219
+ SZ_FAKED: Final = "faked"
220
+ SZ_SCHEME: Final = "scheme"
215
221
 
216
- SZ_BLOCK_LIST = "block_list"
217
- SZ_KNOWN_LIST = "known_list"
222
+ SZ_BLOCK_LIST: Final = "block_list"
223
+ SZ_KNOWN_LIST: Final = "known_list"
218
224
 
219
225
  SCH_DEVICE_ID_ANY = vol.Match(DEVICE_ID_REGEX.ANY)
220
226
  SCH_DEVICE_ID_SEN = vol.Match(DEVICE_ID_REGEX.SEN)
@@ -226,13 +232,23 @@ SCH_DEVICE_ID_BDR = vol.Match(DEVICE_ID_REGEX.BDR)
226
232
  SCH_DEVICE_ID_UFC = vol.Match(DEVICE_ID_REGEX.UFC)
227
233
 
228
234
  _SCH_TRAITS_DOMAINS = ("heat", "hvac")
229
- _SCH_TRAITS_HVAC_SCHEMES = ("itho", "nuaire", "orcon")
235
+ _SCH_TRAITS_HVAC_SCHEMES = ("itho", "nuaire", "orcon", "vasco", "climarad")
236
+
237
+
238
+ DeviceTraitsT = TypedDict(
239
+ "DeviceTraitsT",
240
+ {
241
+ "alias": str | None,
242
+ "faked": bool | None,
243
+ "class": str | None,
244
+ },
245
+ )
230
246
 
231
247
 
232
248
  def sch_global_traits_dict_factory(
233
- heat_traits: dict[vol.Optional, vol.Any] = None,
234
- hvac_traits: dict[vol.Optional, vol.Any] = None,
235
- ) -> tuple[dict[vol.Optional, vol.Any], vol.Schema]:
249
+ heat_traits: dict[vol.Optional, vol.Any] | None = None,
250
+ hvac_traits: dict[vol.Optional, vol.Any] | None = None,
251
+ ) -> tuple[dict[vol.Optional, vol.Any], vol.Any]:
236
252
  """Return a global traits dict with a configurable extra traits.
237
253
 
238
254
  usage:
@@ -254,15 +270,16 @@ def sch_global_traits_dict_factory(
254
270
  extra=vol.PREVENT_EXTRA,
255
271
  )
256
272
 
273
+ # NOTE: voluptuous doesn't like StrEnums, hence str(s)
257
274
  # TIP: the _domain key can be used to force which traits schema to use
258
275
  heat_slugs = list(
259
- s for s in DEV_TYPE_MAP.slugs() if s not in DEV_TYPE_MAP.HVAC_SLUGS
276
+ str(s) for s in DEV_TYPE_MAP.slugs() if s not in DEV_TYPE_MAP.HVAC_SLUGS
260
277
  )
261
278
  SCH_TRAITS_HEAT = SCH_TRAITS_BASE.extend(
262
279
  {
263
280
  vol.Optional("_domain", default="heat"): "heat",
264
281
  vol.Optional(SZ_CLASS): vol.Any(
265
- None, *heat_slugs, *(DEV_TYPE_MAP[s] for s in heat_slugs)
282
+ None, *heat_slugs, *(str(DEV_TYPE_MAP[s]) for s in heat_slugs)
266
283
  ),
267
284
  }
268
285
  )
@@ -271,17 +288,18 @@ def sch_global_traits_dict_factory(
271
288
  extra=vol.PREVENT_EXTRA if heat_traits else vol.REMOVE_EXTRA,
272
289
  )
273
290
 
274
- hvac_slugs = DEV_TYPE_MAP.HVAC_SLUGS
291
+ # NOTE: voluptuous doesn't like StrEnums, hence str(s)
292
+ hvac_slugs = list(str(s) for s in DEV_TYPE_MAP.HVAC_SLUGS)
275
293
  SCH_TRAITS_HVAC = SCH_TRAITS_BASE.extend(
276
294
  {
277
295
  vol.Optional("_domain", default="hvac"): "hvac",
278
296
  vol.Optional(SZ_CLASS, default="HVC"): vol.Any(
279
- None, *hvac_slugs, *(DEV_TYPE_MAP[s] for s in hvac_slugs)
297
+ None, *hvac_slugs, *(str(DEV_TYPE_MAP[s]) for s in hvac_slugs)
280
298
  ), # TODO: consider removing None
281
299
  }
282
300
  )
283
301
  SCH_TRAITS_HVAC = SCH_TRAITS_HVAC.extend(
284
- {vol.Optional("scheme"): vol.Any(*_SCH_TRAITS_HVAC_SCHEMES)}
302
+ {vol.Optional(SZ_SCHEME): vol.Any(*_SCH_TRAITS_HVAC_SCHEMES)}
285
303
  )
286
304
  SCH_TRAITS_HVAC = SCH_TRAITS_HVAC.extend(
287
305
  hvac_traits,
@@ -318,84 +336,83 @@ SCH_GLOBAL_TRAITS_DICT, SCH_TRAITS = sch_global_traits_dict_factory()
318
336
  # Device lists (Engine configuration)
319
337
 
320
338
 
339
+ DeviceIdT = NewType("DeviceIdT", str) # TypeVar('DeviceIdT', bound=str) #
340
+ DevIndexT = NewType("DevIndexT", str)
341
+ DeviceListT: TypeAlias = dict[DeviceIdT, DeviceTraitsT]
342
+
343
+
321
344
  def select_device_filter_mode(
322
- enforce_known_list: bool, known_list: list, block_list: list
345
+ enforce_known_list: bool,
346
+ known_list: DeviceListT,
347
+ block_list: DeviceListT,
323
348
  ) -> bool:
324
349
  """Determine which device filter to use, if any.
325
350
 
326
351
  Either:
352
+ - block if device_id in block_list (could be empty), otherwise
327
353
  - allow if device_id in known_list, or
328
- - block if device_id in block_list (could be empty)
329
354
  """
330
355
 
331
- if both := set(known_list) & set(block_list):
332
- raise ValueError(
333
- f"There are devices in both the {SZ_KNOWN_LIST} & {SZ_BLOCK_LIST}: {both}"
334
- )
335
-
336
- hgi_list = [
337
- k
338
- for k, v in known_list.items()
339
- if k[:2] == DEV_TYPE_MAP._hex(DEV_TYPE.HGI)
340
- and v.get(SZ_CLASS) in (None, DEV_TYPE.HGI, DEV_TYPE_MAP[DEV_TYPE.HGI])
341
- ]
342
- if len(hgi_list) != 1:
343
- _LOGGER.warning(
344
- f"Best practice is exactly one gateway (HGI) in the {SZ_KNOWN_LIST}: %s",
345
- hgi_list,
346
- )
356
+ # warn if not has_exactly_one_valid_hgi(known_list)
347
357
 
348
358
  if enforce_known_list and not known_list:
349
359
  _LOGGER.warning(
350
- f"An empty {SZ_KNOWN_LIST} was provided, so it cant be used "
351
- f"as a whitelist (device_id filter)"
360
+ f"Best practice is to enforce a {SZ_KNOWN_LIST} (an allow list), "
361
+ f"but it is empty, so it can't be used "
352
362
  )
353
363
  enforce_known_list = False
354
364
 
355
365
  if enforce_known_list:
356
366
  _LOGGER.info(
357
- f"The {SZ_KNOWN_LIST} will be used "
358
- f"as a whitelist (device_id filter), length = {len(known_list)}"
367
+ f"A valid {SZ_KNOWN_LIST} was provided, "
368
+ f"and will be enforced as a allow list: length = {len(known_list)}"
359
369
  )
360
370
  _LOGGER.debug(f"known_list = {known_list}")
361
371
 
362
372
  elif block_list:
363
373
  _LOGGER.info(
364
- f"The {SZ_BLOCK_LIST} will be used "
365
- f"as a blacklist (device_id filter), length = {len(block_list)}"
374
+ f"A valid {SZ_BLOCK_LIST} was provided, "
375
+ f"and will be used as a deny list: length = {len(block_list)}"
366
376
  )
367
377
  _LOGGER.debug(f"block_list = {block_list}")
368
378
 
369
379
  elif known_list:
370
380
  _LOGGER.warning(
371
- f"It is strongly recommended to use the {SZ_KNOWN_LIST} "
372
- f"as a whitelist (device_id filter), configure: {SZ_ENFORCE_KNOWN_LIST} = True"
381
+ f"Best practice is to enforce the {SZ_KNOWN_LIST} as an allow list, "
382
+ f"configure: {SZ_ENFORCE_KNOWN_LIST} = True"
373
383
  )
374
384
  _LOGGER.debug(f"known_list = {known_list}")
375
385
 
376
386
  else:
377
387
  _LOGGER.warning(
378
- f"It is strongly recommended to provide a {SZ_KNOWN_LIST}, and use it "
379
- f"as a whitelist (device_id filter), configure: {SZ_ENFORCE_KNOWN_LIST} = True"
388
+ f"Best practice is to provide a {SZ_KNOWN_LIST} and enforce it, "
389
+ f"configure: {SZ_ENFORCE_KNOWN_LIST} = True"
380
390
  )
381
391
 
382
392
  return enforce_known_list
383
393
 
384
394
 
385
395
  #
386
- # 4/5: Gateway (engine) configuration
387
- SZ_DISABLE_SENDING = "disable_sending"
388
- SZ_ENFORCE_KNOWN_LIST = f"enforce_{SZ_KNOWN_LIST}"
389
- SZ_EVOFW_FLAG = "evofw_flag"
390
- SZ_USE_REGEX = "use_regex"
396
+ # 5/5: Gateway (engine) configuration
397
+ SZ_DISABLE_SENDING: Final = "disable_sending"
398
+ SZ_DISABLE_QOS: Final = "disable_qos"
399
+ SZ_ENFORCE_KNOWN_LIST: Final[str] = f"enforce_{SZ_KNOWN_LIST}"
400
+ SZ_EVOFW_FLAG: Final = "evofw_flag"
401
+ SZ_USE_REGEX: Final = "use_regex"
391
402
 
392
403
  SCH_ENGINE_DICT = {
393
404
  vol.Optional(SZ_DISABLE_SENDING, default=False): bool,
405
+ vol.Optional(SZ_DISABLE_QOS, default=None): vol.Any(
406
+ None, # None is selective QoS (e.g. QoS only for bindings, schedule, etc.)
407
+ bool,
408
+ ), # in long term, this default to be True (and no None)
394
409
  vol.Optional(SZ_ENFORCE_KNOWN_LIST, default=False): bool,
395
410
  vol.Optional(SZ_EVOFW_FLAG): vol.Any(None, str),
396
411
  # vol.Optional(SZ_PORT_CONFIG): SCH_SERIAL_PORT_CONFIG,
397
412
  vol.Optional(SZ_USE_REGEX): dict, # vol.All(ConvertNullToDict(), dict),
413
+ vol.Optional(SZ_COMMS_PARAMS): SCH_COMMS_PARAMS,
398
414
  }
415
+ SCH_ENGINE_CONFIG = vol.Schema(SCH_ENGINE_DICT, extra=vol.REMOVE_EXTRA)
399
416
 
400
- SZ_INBOUND = "inbound" # for use_regex (intentionally obscured)
401
- SZ_OUTBOUND = "outbound"
417
+ SZ_INBOUND: Final = "inbound" # for use_regex (intentionally obscured)
418
+ SZ_OUTBOUND: Final = "outbound"