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
16home_io_control_ns = cg.esphome_ns.namespace(
"home_io_control")
19CONF_UI_CONTROLS =
"ui_controls"
22_COMPANION_ID_BASE =
"home_io_control"
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"
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"
55TuningConfig = home_io_control_ns.class_(
"TuningConfig")
56IOHomeTuningNumber = home_io_control_ns.class_(
57 "IOHomeTuningNumber", number.Number, cg.Component
59IOHomeTuningSelect = home_io_control_ns.class_(
60 "IOHomeTuningSelect", select.Select, cg.Component
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)
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,
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,
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,
107DISCOVERY_COMMAND_OPTIONS = {
108 "0x28": DiscoveryCommand.DISCOVER,
109 "0x2E": DiscoveryCommand.DISCOVER_ALT,
120DISCOVERY_COMMAND_PRESETS = [
128DISCOVERY_DESTINATION_OPTIONS = [
"auto",
"0x00003B",
"0x00003F"]
130PAIRING_DISCOVERY_PAYLOAD_OPTIONS = [
"none",
"0x00"]
133 """Config-dict key under which a parameter's injected companion entity ID is stored."""
134 return f
"_{param_key}_id"
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",
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"),
185 CONF_SX1262_DISCOVERY_HOP_SLICE_MS: (0, 500, 1,
"ms"),
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"),
197 CONF_COLD_BROADCAST_REPLY_PREAMBLE: (8, 256, 1,
"B"),
203 CONF_NORMAL_START_PREAMBLE: (8, 256, 1,
"B"),
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"),
215 CONF_EXCHANGE_START_RESPONSE_WAIT_MS: (200, 4000, 50,
"ms"),
216 CONF_EXCHANGE_RESPONSE_WAIT_MS: (200, 4000, 50,
"ms"),
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,
""),
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),
239 """Build a validator that requires a string value present in `options`.
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.
248 if coerce_number
and isinstance(value, (int, float))
and not isinstance(value, bool):
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)}")
259 CONF_SX1262_RX_BANDWIDTH, SX1262_BANDWIDTH_OPTIONS, coerce_number=
True
262 CONF_SX1276_RX_BANDWIDTH, SX1276_BANDWIDTH_OPTIONS, coerce_number=
True
265 CONF_LR1121_RX_BANDWIDTH, LR1121_BANDWIDTH_OPTIONS, coerce_number=
True
268 f
"{CONF_PAIRING_DISCOVERY_COMMANDS} entries", DISCOVERY_COMMAND_OPTIONS
271 CONF_PAIRING_DISCOVERY_DESTINATION, DISCOVERY_DESTINATION_OPTIONS
274 CONF_PAIRING_DISCOVERY_PAYLOAD, PAIRING_DISCOVERY_PAYLOAD_OPTIONS
279 """Convert an explicit destination string like '0x00003B' to a list of 3 byte values."""
281 return [int(digits[i : i + 2], 16)
for i
in range(0, 6, 2)]
285 """Convert a payload option string like '0x00' to its numeric byte value."""
286 return int(value, 16)
295TUNING_SCHEMA = cv.Schema(
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),
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,
310 cv.Optional(key): cv.int_range(min=lo, max=hi)
311 for key, (lo, hi, _step, _unit)
in _NUMBER_PARAMS.items()
318 """Declare companion entity IDs for tuning UI controls.
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.
325 if not config[CONF_UI_CONTROLS]:
331 base = _COMPANION_ID_BASE
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)
341TUNING_CONFIG_SCHEMA = cv.All(TUNING_SCHEMA, _inject_tuning_companion_ids)
345 """Format a Python bool as a C++ boolean literal."""
346 return "true" if value
else "false"
350 """Emit a `struct.field = value;` C++ assignment statement at codegen time.
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).
356 cg.add(cg.RawExpression(f
"{struct}.{field} = {value}"))
360 """Generate C++ code that builds a TuningConfig from the validated YAML.
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.
366 tuning_id = cv.declare_id(TuningConfig)(
"tuning_config")
367 tuning = cg.new_variable(tuning_id, cg.RawExpression(f
"{TuningConfig}()"))
370 _assign(tuning,
"active",
"true")
373 if CONF_SX1262_RX_BANDWIDTH
in config:
376 "sx1262_rx_bandwidth",
377 SX1262_BANDWIDTH_OPTIONS[config[CONF_SX1262_RX_BANDWIDTH]],
380 if CONF_SX1276_RX_BANDWIDTH
in config:
383 "sx1276_rx_bandwidth",
384 SX1276_BANDWIDTH_OPTIONS[config[CONF_SX1276_RX_BANDWIDTH]],
387 if CONF_LR1121_RX_BANDWIDTH
in config:
390 "lr1121_rx_bandwidth",
391 LR1121_BANDWIDTH_OPTIONS[config[CONF_LR1121_RX_BANDWIDTH]],
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]:
400 tuning.pairing_discovery_commands.push_back(
401 DISCOVERY_COMMAND_OPTIONS[cmd]
405 if CONF_PAIRING_DISCOVERY_DESTINATION
in config:
406 dest = config[CONF_PAIRING_DISCOVERY_DESTINATION]
408 _assign(tuning,
"pairing_discovery_destination_auto",
"true")
410 _assign(tuning,
"pairing_discovery_destination_auto",
"false")
414 "pairing_discovery_destination",
415 f
"{{0x{b[0]:02X}, 0x{b[1]:02X}, 0x{b[2]:02X}}}",
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")
423 _assign(tuning,
"pairing_discovery_payload_enabled",
"true")
424 _assign(tuning,
"pairing_discovery_payload", f
"0x{_parse_payload(payload):02X}")
426 if CONF_PAIRING_DISCOVERY_LOW_POWER
in config:
429 "pairing_discovery_low_power",
430 _cpp_bool(config[CONF_PAIRING_DISCOVERY_LOW_POWER]),
434 for key
in _NUMBER_PARAMS:
436 _assign(tuning, key, config[key])
438 cg.add(var.set_tuning_config(tuning))
442 """Generate number/select entities for the tuning parameters when enabled."""
443 if not config[CONF_UI_CONTROLS]:
447 for key, options
in _SELECT_OPTIONS.items():
451 for key, (min_value, max_value, step, unit)
in _NUMBER_PARAMS.items():
453 config, var, key, min_value=min_value, max_value=max_value, step=step, unit=unit
458 config, var, key, min_value, max_value, step, unit=""
460 """Create a single IOHomeTuningNumber entity.
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.
466 schema = number.number_schema(
468 unit_of_measurement=unit,
469 entity_category=ENTITY_CATEGORY_CONFIG,
471 entity_config = schema.extend(cv.COMPONENT_SCHEMA)(
474 CONF_NAME: UI_NAMES[key],
477 entity = await number.new_number(
485 await cg.register_component(entity, entity_config)
489 """Create a single IOHomeTuningSelect entity.
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.
494 entity_config = select.select_schema(
495 IOHomeTuningSelect, entity_category=ENTITY_CATEGORY_CONFIG
496 ).extend(cv.COMPONENT_SCHEMA)(
499 CONF_NAME: UI_NAMES[key],
502 entity = await select.new_select(
508 await cg.register_component(entity, entity_config)
512 """Generate code for the tuning block."""
_inject_tuning_companion_ids(config)
_one_of_string(param_name, options, coerce_number=False)
_create_select(config, var, key, options)
_create_tuning_entities(config, var)
_apply_tuning_config(config, var)
_parse_destination_to_bytes(value)
_create_number(config, var, key, min_value, max_value, step, unit="")
_assign(struct, field, value)