Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
tuning.py
Go to the documentation of this file.
1## @file
2## @brief ESPHome tuning schema and code generation for Home IO Control.
3## @ingroup hioc_codegen
4##
5## Provides the YAML ``tuning:`` block under ``home_io_control:`` and optionally
6## generates Home Assistant ``number`` and ``select`` entities so users can adjust
7## pairing and radio parameters at runtime. Tuned values are volatile and reset on
8## every boot.
9
10import esphome.codegen as cg
11import esphome.config_validation as cv
12from esphome.components import number, select
13from esphome.const import CONF_ID, CONF_NAME, ENTITY_CATEGORY_CONFIG
14from esphome.core import ID
15
16home_io_control_ns = cg.esphome_ns.namespace("home_io_control")
17
18CONF_TUNING = "tuning"
19CONF_UI_CONTROLS = "ui_controls"
20
21# Fixed ID prefix for the generated tuning companion entities (see _inject_tuning_companion_ids).
22_COMPANION_ID_BASE = "home_io_control"
23
24# --- Radio / physical layer ---
25CONF_SX1262_RX_BANDWIDTH = "sx1262_rx_bandwidth"
26CONF_SX1262_RESPONSE_PREAMBLE = "sx1262_response_preamble"
27CONF_SX1262_POST_TX_SETTLE_US = "sx1262_post_tx_settle_us"
28CONF_SX1276_RX_BANDWIDTH = "sx1276_rx_bandwidth"
29CONF_SX1276_RESPONSE_PREAMBLE = "sx1276_response_preamble"
30CONF_SX1276_DISCOVERY_HOP_SLICE_MS = "sx1276_discovery_hop_slice_ms"
31CONF_SX1262_DISCOVERY_HOP_SLICE_MS = "sx1262_discovery_hop_slice_ms"
32CONF_LR1121_RX_BANDWIDTH = "lr1121_rx_bandwidth"
33CONF_LR1121_RESPONSE_PREAMBLE = "lr1121_response_preamble"
34CONF_LR1121_POST_TX_SETTLE_US = "lr1121_post_tx_settle_us"
35CONF_LR1121_DISCOVERY_HOP_SLICE_MS = "lr1121_discovery_hop_slice_ms"
36CONF_COLD_BROADCAST_REPLY_PREAMBLE = "cold_broadcast_reply_preamble"
37CONF_NORMAL_START_PREAMBLE = "normal_start_preamble"
38CONF_LBT_MAX_RETRIES = "lbt_max_retries"
39CONF_LBT_RSSI_THRESHOLD_DBM = "lbt_rssi_threshold_dbm"
40CONF_EXCHANGE_START_RESPONSE_WAIT_MS = "exchange_start_response_wait_ms"
41CONF_EXCHANGE_RESPONSE_WAIT_MS = "exchange_response_wait_ms"
42CONF_EXCHANGE_TOTAL_BUDGET_MS = "exchange_total_budget_ms"
43
44# --- Pairing protocol ---
45CONF_PAIRING_DISCOVERY_COMMANDS = "pairing_discovery_commands"
46CONF_PAIRING_DISCOVERY_DESTINATION = "pairing_discovery_destination"
47CONF_PAIRING_DISCOVERY_PAYLOAD = "pairing_discovery_payload"
48CONF_PAIRING_DISCOVERY_LOW_POWER = "pairing_discovery_low_power"
49CONF_PAIRING_DISCOVERY_PREAMBLE = "pairing_discovery_preamble"
50CONF_PAIRING_DISCOVERY_WAIT_MS = "pairing_discovery_wait_ms"
51CONF_PAIRING_DISCOVERY_INITIAL_DWELL_MS = "pairing_discovery_initial_dwell_ms"
52CONF_PAIRING_KEY_EXCHANGE_RETRIES = "pairing_key_exchange_retries"
53
54# C++ type references
55TuningConfig = home_io_control_ns.class_("TuningConfig")
56IOHomeTuningNumber = home_io_control_ns.class_(
57 "IOHomeTuningNumber", number.Number, cg.Component
58)
59IOHomeTuningSelect = home_io_control_ns.class_(
60 "IOHomeTuningSelect", select.Select, cg.Component
61)
62
63# C++ enums. These stringify to their fully-qualified C++ names (e.g.
64# esphome::home_io_control::SX1262RxBandwidth::BW_117_3_KHZ) so generated code compiles in
65# the global-namespace setup() function.
66SX1262RxBandwidth = home_io_control_ns.enum("SX1262RxBandwidth", is_class=True)
67SX1276RxBandwidth = home_io_control_ns.enum("SX1276RxBandwidth", is_class=True)
68LR1121RxBandwidth = home_io_control_ns.enum("LR1121RxBandwidth", is_class=True)
69DiscoveryCommand = home_io_control_ns.enum("DiscoveryCommand", is_class=True)
70
71# Map each YAML option string to its C++ enum value. Options are bare kHz numbers (the "kHz"
72# unit lives in the entity name) for uniformity with the numeric parameters.
73SX1262_BANDWIDTH_OPTIONS = {
74 "39.0": SX1262RxBandwidth.BW_39_0_KHZ,
75 "46.9": SX1262RxBandwidth.BW_46_9_KHZ,
76 "58.6": SX1262RxBandwidth.BW_58_6_KHZ,
77 "78.2": SX1262RxBandwidth.BW_78_2_KHZ,
78 "117.3": SX1262RxBandwidth.BW_117_3_KHZ,
79 "156.2": SX1262RxBandwidth.BW_156_2_KHZ,
80 "187.2": SX1262RxBandwidth.BW_187_2_KHZ,
81}
82
83SX1276_BANDWIDTH_OPTIONS = {
84 "20.8": SX1276RxBandwidth.BW_20_8_KHZ,
85 "41.7": SX1276RxBandwidth.BW_41_7_KHZ,
86 "62.5": SX1276RxBandwidth.BW_62_5_KHZ,
87 "83.3": SX1276RxBandwidth.BW_83_3_KHZ,
88 "125.0": SX1276RxBandwidth.BW_125_0_KHZ,
89}
90
91# LR1121 shares the Semtech GFSK bandwidth grid with SX1262, so LR1121RxBandwidth is
92# byte-for-byte identical to SX1262RxBandwidth today (tuning_config.h; the
93# Sx1262AndLr1121BandwidthTablesAgree test pins them together). They are kept as separate C++
94# enums, and separate option dicts here, only so each chip's option set can diverge if a real
95# chip difference ever demands it. The two narrowest options (39.0/46.9 kHz) are offered on
96# both chips, to get closer to SX1276's real-hardware-validated 41.7 kHz default.
97LR1121_BANDWIDTH_OPTIONS = {
98 "39.0": LR1121RxBandwidth.BW_39_0_KHZ,
99 "46.9": LR1121RxBandwidth.BW_46_9_KHZ,
100 "58.6": LR1121RxBandwidth.BW_58_6_KHZ,
101 "78.2": LR1121RxBandwidth.BW_78_2_KHZ,
102 "117.3": LR1121RxBandwidth.BW_117_3_KHZ,
103 "156.2": LR1121RxBandwidth.BW_156_2_KHZ,
104 "187.2": LR1121RxBandwidth.BW_187_2_KHZ,
105}
106
107DISCOVERY_COMMAND_OPTIONS = {
108 "0x28": DiscoveryCommand.DISCOVER,
109 "0x2E": DiscoveryCommand.DISCOVER_ALT,
110}
111# 0x2A (CMD_DISCOVER_SPE_REQ) is deliberately excluded: it is a roll-call answered only by devices
112# that already hold the controller's system key, so a device in learning mode never answers it.
113# Offering it here could only add replies from already-paired devices to a pairing attempt, never
114# help reach the unpaired one. The command byte itself stays valid — see
115# DiscoveryCommand::DISCOVER_SPE in tuning_config.h.
116
117# Home Assistant `select` entities are single-choice, but the discovery phase can send an
118# ordered list of commands. These comma-separated presets expose the useful combinations as
119# selectable options; the C++ dispatch (update_tuning_select) parses them back into the vector.
120DISCOVERY_COMMAND_PRESETS = [
121 "0x28", # default 2W discovery
122 "0x2E", # alternate discovery — kept for completeness/experimentation, but broadcast 0x2E has
123 # never drawn a response from any device this project has real evidence for; see the
124 # CMD_DISCOVER_ALT_REQ doc comment in proto_constants.h. Not a recommended fix.
125 "0x28,0x2E", # both broadcasts
126]
127
128DISCOVERY_DESTINATION_OPTIONS = ["auto", "0x00003B", "0x00003F"]
129
130PAIRING_DISCOVERY_PAYLOAD_OPTIONS = ["none", "0x00"]
131
132def _id_key(param_key):
133 """Config-dict key under which a parameter's injected companion entity ID is stored."""
134 return f"_{param_key}_id"
135
136
137# UI entity names. Radio params are prefixed "Radio" and pairing params "Pairing" so that,
138# under Home Assistant's Configuration section (entity_category=config), the two logical groups
139# cluster together alphabetically.
140UI_NAMES = {
141 CONF_SX1262_RX_BANDWIDTH: "Radio SX1262 RX Bandwidth (kHz)",
142 CONF_SX1262_RESPONSE_PREAMBLE: "Radio SX1262 Response Preamble",
143 CONF_SX1262_POST_TX_SETTLE_US: "Radio SX1262 Post-TX Settle",
144 CONF_SX1276_RX_BANDWIDTH: "Radio SX1276 RX Bandwidth (kHz)",
145 CONF_SX1276_RESPONSE_PREAMBLE: "Radio SX1276 Response Preamble",
146 CONF_SX1276_DISCOVERY_HOP_SLICE_MS: "Radio SX1276 Discovery Hop Slice",
147 CONF_SX1262_DISCOVERY_HOP_SLICE_MS: "Radio SX1262 Discovery Hop Slice",
148 CONF_LR1121_RX_BANDWIDTH: "Radio LR1121 RX Bandwidth (kHz)",
149 CONF_LR1121_RESPONSE_PREAMBLE: "Radio LR1121 Response Preamble",
150 CONF_LR1121_POST_TX_SETTLE_US: "Radio LR1121 Post-TX Settle",
151 CONF_LR1121_DISCOVERY_HOP_SLICE_MS: "Radio LR1121 Discovery Hop Slice",
152 CONF_COLD_BROADCAST_REPLY_PREAMBLE: "Radio Cold Broadcast Reply Preamble",
153 CONF_NORMAL_START_PREAMBLE: "Radio Normal Start Preamble",
154 CONF_LBT_MAX_RETRIES: "Radio LBT Max Retries",
155 CONF_LBT_RSSI_THRESHOLD_DBM: "Radio LBT RSSI Threshold",
156 CONF_EXCHANGE_START_RESPONSE_WAIT_MS: "Exchange Start Response Wait",
157 CONF_EXCHANGE_RESPONSE_WAIT_MS: "Exchange Response Wait",
158 CONF_EXCHANGE_TOTAL_BUDGET_MS: "Exchange Total Budget",
159 CONF_PAIRING_DISCOVERY_COMMANDS: "Pairing Discovery Commands",
160 CONF_PAIRING_DISCOVERY_DESTINATION: "Pairing Discovery Destination",
161 CONF_PAIRING_DISCOVERY_PAYLOAD: "Pairing Discovery Payload",
162 CONF_PAIRING_DISCOVERY_LOW_POWER: "Pairing Discovery Low Power",
163 CONF_PAIRING_DISCOVERY_PREAMBLE: "Pairing Discovery Preamble",
164 CONF_PAIRING_DISCOVERY_WAIT_MS: "Pairing Discovery Wait",
165 CONF_PAIRING_DISCOVERY_INITIAL_DWELL_MS: "Pairing Discovery Initial Dwell",
166 CONF_PAIRING_KEY_EXCHANGE_RETRIES: "Pairing Key Exchange Retries",
167}
168
169# Numeric parameters: key -> (min, max, step, unit). Single source of truth for both the
170# YAML validation range and the Home Assistant `number` entity bounds, so the two cannot drift.
171_NUMBER_PARAMS = {
172 # Floor is 8, not an arbitrary round number: it's SHORT_PREAMBLE, the protocol's own nominal
173 # preamble length and the lowest value that makes sense to transmit (below it there isn't
174 # enough preamble left for the peer's detector to lock on at all). Same floor as
175 # sx1276_response_preamble below.
176 CONF_SX1262_RESPONSE_PREAMBLE: (8, 256, 1, "B"),
177 CONF_SX1262_POST_TX_SETTLE_US: (0, 2000, 10, "µs"),
178 CONF_SX1276_RESPONSE_PREAMBLE: (8, 256, 1, "B"),
179 CONF_SX1276_DISCOVERY_HOP_SLICE_MS: (5, 200, 1, "ms"),
180 # Floor is 0, not a physically meaningful minimum for the chip: coverage degrades gradually as
181 # the dwell shortens and only truly collapses at the literal 0 ms edge case, where
182 # wait_for_packet(..., 0) returns before any guard can observe activity at all. Left open so
183 # that floor stays empirically checkable rather than assumed. See
184 # SX1262_DISCOVERY_HOP_SLICE_MS in tuning_config.h for why the default itself is short.
185 CONF_SX1262_DISCOVERY_HOP_SLICE_MS: (0, 500, 1, "ms"),
186 # LR1121 numeric ranges reuse the SX1262 bounds — same chip-family physical constraints, and
187 # a validated SX1262 value encodes protocol-side reality more than a chip quirk.
188 CONF_LR1121_RESPONSE_PREAMBLE: (8, 256, 1, "B"),
189 CONF_LR1121_POST_TX_SETTLE_US: (0, 2000, 10, "µs"),
190 CONF_LR1121_DISCOVERY_HOP_SLICE_MS: (0, 500, 1, "ms"),
191 # Same (8, 256) floor/ceiling as sx1262_response_preamble above, for the same reason: 8 is
192 # SHORT_PREAMBLE, the lowest value that leaves the peer's detector anything to lock on to.
193 # This field is chip-neutral (see proto_timing.h COLD_BROADCAST_REPLY_PREAMBLE, which lives
194 # there rather than in tuning_config.h precisely because it is chip-neutral -- see that header's
195 # own "chip-neutral defaults live in proto_timing.h" comment) so it has no per-chip range to
196 # inherit the way lr1121_response_preamble does.
197 CONF_COLD_BROADCAST_REPLY_PREAMBLE: (8, 256, 1, "B"),
198 # Same (8, 256) floor/ceiling as the response-preamble params: 8 is SHORT_PREAMBLE, the lowest
199 # value that leaves the peer's detector anything to lock on to. The default is 32
200 # (NORMAL_START_PREAMBLE in proto_timing.h) -- 256 bits, inside the preamble band the protocol
201 # reference documents -- not 8: brand-new devices have been seen failing at 1/4/8 B
202 # (docs/radio_diagnostics.md), so 8 is a proven-safe floor for the knob, not a sensible default.
203 CONF_NORMAL_START_PREAMBLE: (8, 256, 1, "B"),
204 # Floor is 8 (SHORT_PREAMBLE), same reasoning as the response-preamble params above. Ceiling is
205 # the current default (LONG_PREAMBLE, 1024) rather than 256 like the other preamble knobs: this
206 # one exists specifically to let a stuck pairing attempt go *shorter* than the long wake-up
207 # burst (issue #27/#87's precedent), not to go longer than it — nothing calls for raising it
208 # further.
209 CONF_PAIRING_DISCOVERY_PREAMBLE: (8, 1024, 1, "B"),
210 CONF_LBT_MAX_RETRIES: (0, 10, 1, ""),
211 CONF_LBT_RSSI_THRESHOLD_DBM: (-95, -70, 1, "dBm"),
212 # Ceilings are generous because the right value is a property of the target device, not of
213 # the radio: measured RS100 solar reply latencies reach ~3 s (see RESPONSE_START_WAIT_MS).
214 # Every millisecond here is loop-blocking time on a *failed* exchange only (ADR 0013).
215 CONF_EXCHANGE_START_RESPONSE_WAIT_MS: (200, 4000, 50, "ms"),
216 CONF_EXCHANGE_RESPONSE_WAIT_MS: (200, 4000, 50, "ms"),
217 # Ceiling on a whole exchange. Every millisecond over ~2550 blocks the ESPHome loop past its
218 # own warning threshold, so raise this only when persistence matters more than responsiveness.
219 CONF_EXCHANGE_TOTAL_BUDGET_MS: (500, 12000, 100, "ms"),
220 CONF_PAIRING_DISCOVERY_WAIT_MS: (500, 5000, 50, "ms"),
221 CONF_PAIRING_DISCOVERY_INITIAL_DWELL_MS: (0, 500, 10, "ms"),
222 CONF_PAIRING_KEY_EXCHANGE_RETRIES: (1, 5, 1, ""),
223}
224
225# Select parameters: key -> ordered option list. Single source of truth for both the
226# companion-ID injection and the entity creation.
227_SELECT_OPTIONS = {
228 CONF_PAIRING_DISCOVERY_COMMANDS: DISCOVERY_COMMAND_PRESETS,
229 CONF_PAIRING_DISCOVERY_DESTINATION: DISCOVERY_DESTINATION_OPTIONS,
230 CONF_PAIRING_DISCOVERY_PAYLOAD: PAIRING_DISCOVERY_PAYLOAD_OPTIONS,
231 CONF_PAIRING_DISCOVERY_LOW_POWER: ["Off", "On"],
232 CONF_SX1262_RX_BANDWIDTH: list(SX1262_BANDWIDTH_OPTIONS),
233 CONF_SX1276_RX_BANDWIDTH: list(SX1276_BANDWIDTH_OPTIONS),
234 CONF_LR1121_RX_BANDWIDTH: list(LR1121_BANDWIDTH_OPTIONS),
235}
236
237
238def _one_of_string(param_name, options, coerce_number=False):
239 """Build a validator that requires a string value present in `options`.
240
241 Works for both list and dict `options` (dict membership tests the keys), so a single
242 factory replaces a hand-written validator per option set while keeping a per-parameter
243 error message. With `coerce_number`, a numeric YAML value (e.g. ``117.3``) is accepted and
244 normalized to its string form, so bare-number options do not force the user to quote them.
245 """
246
247 def validate(value):
248 if coerce_number and isinstance(value, (int, float)) and not isinstance(value, bool):
249 value = str(value)
250 value = cv.string_strict(value)
251 if value not in options:
252 raise cv.Invalid(f"{param_name} must be one of {list(options)}")
253 return value
254
255 return validate
256
257
258_validate_bandwidth = _one_of_string(
259 CONF_SX1262_RX_BANDWIDTH, SX1262_BANDWIDTH_OPTIONS, coerce_number=True
260)
261_validate_sx1276_bandwidth = _one_of_string(
262 CONF_SX1276_RX_BANDWIDTH, SX1276_BANDWIDTH_OPTIONS, coerce_number=True
263)
264_validate_lr1121_bandwidth = _one_of_string(
265 CONF_LR1121_RX_BANDWIDTH, LR1121_BANDWIDTH_OPTIONS, coerce_number=True
266)
267_validate_discovery_command = _one_of_string(
268 f"{CONF_PAIRING_DISCOVERY_COMMANDS} entries", DISCOVERY_COMMAND_OPTIONS
269)
270_validate_discovery_destination = _one_of_string(
271 CONF_PAIRING_DISCOVERY_DESTINATION, DISCOVERY_DESTINATION_OPTIONS
272)
273_validate_discovery_payload = _one_of_string(
274 CONF_PAIRING_DISCOVERY_PAYLOAD, PAIRING_DISCOVERY_PAYLOAD_OPTIONS
275)
276
277
279 """Convert an explicit destination string like '0x00003B' to a list of 3 byte values."""
280 digits = value[2:] # strip the '0x' prefix
281 return [int(digits[i : i + 2], 16) for i in range(0, 6, 2)]
282
283
284def _parse_payload(value):
285 """Convert a payload option string like '0x00' to its numeric byte value."""
286 return int(value, 16)
287
288
289# Tuning sub-schema (imported and extended by the hub __init__.py).
290#
291# Only `ui_controls` carries a Python default (it is a feature toggle, not a tunable). Every
292# tunable is a plain cv.Optional with NO default: when a key is omitted, the C++ TuningConfig
293# field keeps its canonical default from proto_frame.h, which is the single source of truth.
294# to_code() only overrides fields that are actually present in the validated config.
295TUNING_SCHEMA = cv.Schema(
296 {
297 cv.Optional(CONF_UI_CONTROLS, default=False): cv.boolean,
298 cv.Optional(CONF_SX1262_RX_BANDWIDTH): _validate_bandwidth,
299 cv.Optional(CONF_SX1276_RX_BANDWIDTH): _validate_sx1276_bandwidth,
300 cv.Optional(CONF_LR1121_RX_BANDWIDTH): _validate_lr1121_bandwidth,
301 cv.Optional(CONF_PAIRING_DISCOVERY_COMMANDS): cv.All(
302 cv.ensure_list(_validate_discovery_command),
303 cv.Length(min=1),
304 ),
305 cv.Optional(CONF_PAIRING_DISCOVERY_DESTINATION): _validate_discovery_destination,
306 cv.Optional(CONF_PAIRING_DISCOVERY_PAYLOAD): _validate_discovery_payload,
307 cv.Optional(CONF_PAIRING_DISCOVERY_LOW_POWER): cv.boolean,
308 # Numeric parameters share their range with the number-entity bounds via _NUMBER_PARAMS.
309 **{
310 cv.Optional(key): cv.int_range(min=lo, max=hi)
311 for key, (lo, hi, _step, _unit) in _NUMBER_PARAMS.items()
312 },
313 }
314)
315
316
318 """Declare companion entity IDs for tuning UI controls.
319
320 ESPHome 2026.x sizes the runtime component vector from the number of IDs
321 declared during validation. If UI entities were created only in to_code(),
322 they could be silently dropped. This post-validator injects declared IDs
323 before the vector is sized.
324 """
325 if not config[CONF_UI_CONTROLS]:
326 return config
327
328 # The tuning block is a sub-schema of home_io_control and does not carry the parent's
329 # CONF_ID, so the companion entity IDs use a fixed prefix. It only needs to be unique
330 # among the generated tuning entities.
331 base = _COMPANION_ID_BASE
332
333 for key in _SELECT_OPTIONS:
334 config[_id_key(key)] = ID(f"{base}_{key}", is_declaration=True, type=IOHomeTuningSelect)
335 for key in _NUMBER_PARAMS:
336 config[_id_key(key)] = ID(f"{base}_{key}", is_declaration=True, type=IOHomeTuningNumber)
337
338 return config
339
340
341TUNING_CONFIG_SCHEMA = cv.All(TUNING_SCHEMA, _inject_tuning_companion_ids)
342
343
344def _cpp_bool(value):
345 """Format a Python bool as a C++ boolean literal."""
346 return "true" if value else "false"
347
348
349def _assign(struct, field, value):
350 """Emit a `struct.field = value;` C++ assignment statement at codegen time.
351
352 ESPHome's MockObj does not support Python attribute assignment, so struct fields
353 are populated with raw assignment statements. `value` must already be valid C++
354 (an int, or a string such as an enum value or brace-init list).
355 """
356 cg.add(cg.RawExpression(f"{struct}.{field} = {value}"))
357
358
359def _apply_tuning_config(config, var):
360 """Generate C++ code that builds a TuningConfig from the validated YAML.
361
362 Only keys the user actually provided are emitted; every omitted key keeps the
363 default baked into the C++ TuningConfig struct (sourced from proto_frame.h), so
364 the defaults are never restated here.
365 """
366 tuning_id = cv.declare_id(TuningConfig)("tuning_config")
367 tuning = cg.new_variable(tuning_id, cg.RawExpression(f"{TuningConfig}()"))
368
369 # Presence of the block is what activates the override layer.
370 _assign(tuning, "active", "true")
371
372 # --- Parameters needing custom handling (enum/list/byte/bool) ---
373 if CONF_SX1262_RX_BANDWIDTH in config:
374 _assign(
375 tuning,
376 "sx1262_rx_bandwidth",
377 SX1262_BANDWIDTH_OPTIONS[config[CONF_SX1262_RX_BANDWIDTH]],
378 )
379
380 if CONF_SX1276_RX_BANDWIDTH in config:
381 _assign(
382 tuning,
383 "sx1276_rx_bandwidth",
384 SX1276_BANDWIDTH_OPTIONS[config[CONF_SX1276_RX_BANDWIDTH]],
385 )
386
387 if CONF_LR1121_RX_BANDWIDTH in config:
388 _assign(
389 tuning,
390 "lr1121_rx_bandwidth",
391 LR1121_BANDWIDTH_OPTIONS[config[CONF_LR1121_RX_BANDWIDTH]],
392 )
393
394 # Ordered discovery commands. Clear the struct default before appending so a
395 # user-provided list replaces it rather than extending it.
396 if CONF_PAIRING_DISCOVERY_COMMANDS in config:
397 cg.add(tuning.pairing_discovery_commands.clear())
398 for cmd in config[CONF_PAIRING_DISCOVERY_COMMANDS]:
399 cg.add(
400 tuning.pairing_discovery_commands.push_back(
401 DISCOVERY_COMMAND_OPTIONS[cmd]
402 )
403 )
404
405 if CONF_PAIRING_DISCOVERY_DESTINATION in config:
406 dest = config[CONF_PAIRING_DISCOVERY_DESTINATION]
407 if dest == "auto":
408 _assign(tuning, "pairing_discovery_destination_auto", "true")
409 else:
410 _assign(tuning, "pairing_discovery_destination_auto", "false")
412 _assign(
413 tuning,
414 "pairing_discovery_destination",
415 f"{{0x{b[0]:02X}, 0x{b[1]:02X}, 0x{b[2]:02X}}}",
416 )
417
418 if CONF_PAIRING_DISCOVERY_PAYLOAD in config:
419 payload = config[CONF_PAIRING_DISCOVERY_PAYLOAD]
420 if payload == "none":
421 _assign(tuning, "pairing_discovery_payload_enabled", "false")
422 else:
423 _assign(tuning, "pairing_discovery_payload_enabled", "true")
424 _assign(tuning, "pairing_discovery_payload", f"0x{_parse_payload(payload):02X}")
425
426 if CONF_PAIRING_DISCOVERY_LOW_POWER in config:
427 _assign(
428 tuning,
429 "pairing_discovery_low_power",
430 _cpp_bool(config[CONF_PAIRING_DISCOVERY_LOW_POWER]),
431 )
432
433 # --- Plain integer parameters: the C++ struct field name equals the YAML key. ---
434 for key in _NUMBER_PARAMS:
435 if key in config:
436 _assign(tuning, key, config[key])
437
438 cg.add(var.set_tuning_config(tuning))
439
440
441async def _create_tuning_entities(config, var):
442 """Generate number/select entities for the tuning parameters when enabled."""
443 if not config[CONF_UI_CONTROLS]:
444 return
445
446 # --- Select entities (options sourced from _SELECT_OPTIONS) ---
447 for key, options in _SELECT_OPTIONS.items():
448 await _create_select(config, var, key, options)
449
450 # --- Number entities (bounds sourced from _NUMBER_PARAMS) ---
451 for key, (min_value, max_value, step, unit) in _NUMBER_PARAMS.items():
452 await _create_number(
453 config, var, key, min_value=min_value, max_value=max_value, step=step, unit=unit
454 )
455
456
458 config, var, key, min_value, max_value, step, unit=""
459):
460 """Create a single IOHomeTuningNumber entity.
461
462 The bare {id, name} dict is normalized through number_schema()+COMPONENT_SCHEMA so it
463 carries the entity/component defaults (disabled_by_default, setup_priority, ...) that
464 register_number()/register_component() require.
465 """
466 schema = number.number_schema(
467 IOHomeTuningNumber,
468 unit_of_measurement=unit,
469 entity_category=ENTITY_CATEGORY_CONFIG,
470 )
471 entity_config = schema.extend(cv.COMPONENT_SCHEMA)(
472 {
473 CONF_ID: config[_id_key(key)],
474 CONF_NAME: UI_NAMES[key],
475 }
476 )
477 entity = await number.new_number(
478 entity_config,
479 var,
480 key,
481 min_value=min_value,
482 max_value=max_value,
483 step=step,
484 )
485 await cg.register_component(entity, entity_config)
486
487
488async def _create_select(config, var, key, options):
489 """Create a single IOHomeTuningSelect entity.
490
491 The bare {id, name} dict is normalized through select_schema()+COMPONENT_SCHEMA so it
492 carries the entity/component defaults that register_select()/register_component() require.
493 """
494 entity_config = select.select_schema(
495 IOHomeTuningSelect, entity_category=ENTITY_CATEGORY_CONFIG
496 ).extend(cv.COMPONENT_SCHEMA)(
497 {
498 CONF_ID: config[_id_key(key)],
499 CONF_NAME: UI_NAMES[key],
500 }
501 )
502 entity = await select.new_select(
503 entity_config,
504 var,
505 key,
506 options=options,
507 )
508 await cg.register_component(entity, entity_config)
509
510
511async def to_code(config, var):
512 """Generate code for the tuning block."""
513 _apply_tuning_config(config, var)
514 await _create_tuning_entities(config, var)
_inject_tuning_companion_ids(config)
Definition tuning.py:317
_one_of_string(param_name, options, coerce_number=False)
Definition tuning.py:238
_create_select(config, var, key, options)
Definition tuning.py:488
_create_tuning_entities(config, var)
Definition tuning.py:441
_apply_tuning_config(config, var)
Definition tuning.py:359
_parse_destination_to_bytes(value)
Definition tuning.py:278
_id_key(param_key)
Definition tuning.py:132
to_code(config, var)
Definition tuning.py:511
_create_number(config, var, key, min_value, max_value, step, unit="")
Definition tuning.py:459
_assign(struct, field, value)
Definition tuning.py:349