13import esphome.codegen
as cg
14import esphome.config_validation
as cv
15from esphome
import pins
16from esphome.components
import spi
25from esphome.components
import button
as button_component
26from esphome.components
import switch
as switch_component
27from esphome.const
import (
33 ENTITY_CATEGORY_CONFIG,
35from esphome.core
import CORE, ID
36from esphome.helpers
import write_file_if_changed
38from .
import lr1121_firmware
39from .
import tuning
as tuning_module
41_LOGGER = logging.getLogger(__name__)
43DEPENDENCIES = [
"api",
"spi"]
44AUTO_LOAD = [
"button",
"cover",
"light",
"lock",
"number",
"select",
"sensor",
"switch",
"text_sensor"]
47CONF_HOME_IO_CONTROL_ID =
"home_io_control_id"
48CONF_RST_PIN =
"rst_pin"
49CONF_DIO0_PIN =
"dio0_pin"
50CONF_DIO4_PIN =
"dio4_pin"
51CONF_DIO1_PIN =
"dio1_pin"
52CONF_BUSY_PIN =
"busy_pin"
53CONF_NODE_ID =
"node_id"
54CONF_SYSTEM_KEY =
"system_key"
55CONF_TX_POWER =
"tx_power"
57CONF_RADIO_TYPE =
"radio_type"
58CONF_FEM_EN_PIN =
"fem_en_pin"
59CONF_VFEM_PIN =
"vfem_pin"
60CONF_FEM_PA_PIN =
"fem_pa_pin"
61CONF_TCXO_VOLTAGE =
"tcxo_voltage"
62CONF_EXPOSED_SENDERS =
"exposed_senders"
63CONF_ACCEPT_FOREIGN_PAIRING =
"accept_foreign_pairing"
64CONF_LR1121_FIRMWARE_UPDATE =
"lr1121_firmware_update"
65CONF_LR1121_BOOTLOADER =
"bootloader"
66CONF_CHECKSUM_MD5 =
"checksum_md5"
67CONF_TARGET_VERSION =
"target_version"
68MIN_STATUS_POLL_INTERVAL_MS = 500
75CONF_ACCEPT_FOREIGN_PAIRING_SWITCH_ID =
"_accept_foreign_pairing_switch_id"
78CONF_LR1121_FIRMWARE_UPDATE_BUTTON_ID =
"_lr1121_firmware_update_button_id"
82CONF_LR1121_BOOTLOADER_SWITCH_ID =
"_lr1121_bootloader_switch_id"
84home_io_control_ns = cg.esphome_ns.namespace(
"home_io_control")
85IOHomeControlComponent = home_io_control_ns.class_(
86 "IOHomeControlComponent", cg.Component, spi.SPIDevice
95IOHomeAcceptForeignPairingSwitch = home_io_control_ns.class_(
96 "IOHomeAcceptForeignPairingSwitch", switch_component.Switch, cg.Component
102IOHomeLr1121FirmwareUpdateButton = home_io_control_ns.class_(
103 "IOHomeLr1121FirmwareUpdateButton", button_component.Button, cg.Component
109IOHomeLr1121BootloaderRewriteSwitch = home_io_control_ns.class_(
110 "IOHomeLr1121BootloaderRewriteSwitch", switch_component.Switch, cg.Component
115 if not config[CONF_ACCEPT_FOREIGN_PAIRING]:
117 parent_id = config[CONF_ID]
118 base = parent_id.id
if parent_id.id
else "home_io_control"
119 config[CONF_ACCEPT_FOREIGN_PAIRING_SWITCH_ID] = ID(
120 f
"{base}_accept_foreign_pairing_switch",
122 type=IOHomeAcceptForeignPairingSwitch,
128 """Validate the lr1121_firmware_update `source:` shorthand at schema time.
130 Checks the shape (github://owner/repo/path[@ref]) and the image class (transceiver vs.
131 loader vs. modem, by filename -- see lr1121_firmware.validate_image_class()). The network
132 fetch and MD5/image-content verification happen later, in to_code(), where a failure is
133 still a build-time error but one that needs the network anyway.
134 @param expect_loader True for the bootloader sub-block's `source:` (must be a loader image),
135 False for the ordinary transceiver `source:` (must not be one).
137 value = cv.string_strict(value)
139 _, _, path, _ = lr1121_firmware.parse_github_source(value)
140 lr1121_firmware.validate_image_class(path, expect_loader=expect_loader)
142 raise cv.Invalid(str(err))
from err
147 """Validate checksum_md5 as exactly 32 hex characters (MD5)."""
148 value = cv.string_strict(value).lower()
150 raise cv.Invalid(
"checksum_md5 must be exactly 32 hex characters (MD5)")
153 except ValueError
as err:
154 raise cv.Invalid(
"checksum_md5 must be valid hexadecimal")
from err
162LR1121_BOOTLOADER_SCHEMA = cv.Schema(
165 cv.Optional(CONF_REF): cv.string_strict,
166 cv.Optional(CONF_CHECKSUM_MD5): validate_checksum_md5,
170LR1121_FIRMWARE_UPDATE_SCHEMA = cv.Schema(
172 cv.Required(CONF_SOURCE): validate_lr1121_firmware_source,
173 cv.Optional(CONF_REF): cv.string_strict,
174 cv.Optional(CONF_CHECKSUM_MD5): validate_checksum_md5,
179 cv.Optional(CONF_TARGET_VERSION): cv.hex_int,
182 cv.Optional(CONF_LR1121_BOOTLOADER): LR1121_BOOTLOADER_SCHEMA,
187def _validate_lr1121_bootloader_block(config):
188 """Implement the build-time compatibility rule for the bootloader: sub-block (ADR 0021).
190 Classifies the *outer* source:'s target against LR1121_KNOWN_BOOTLOADER_REQUIREMENTS without
191 any network access (both source: filenames are already schema-validated shapes at this point,
192 so parsing them again here is free). Deliberately three-way, like the runtime compatibility
193 rule: an unrecognised target warns rather than errors, so the feature doesn't rot on Semtech's
194 next release (see lr1121_firmware.classify_bootloader_upgrade_class()'s doc comment).
196 fw_config = config[CONF_LR1121_FIRMWARE_UPDATE]
197 if CONF_LR1121_BOOTLOADER
not in fw_config:
200 target_fw = lr1121_firmware.resolve_target_version(fw_config[CONF_SOURCE], fw_config.get(CONF_TARGET_VERSION))
201 upgrade_class = lr1121_firmware.classify_bootloader_upgrade_class(target_fw)
202 if upgrade_class ==
"hard_error":
204 f
"lr1121_firmware_update.bootloader: is configured, but source: targets firmware 0x{target_fw:04X}, "
205 "which is known to require bootloader 0x2100 -- after the bootloader rewrite this image would be "
206 "unflashable, so this configuration would arm a trap. Point source: at a firmware version requiring "
207 "bootloader 0x2101 (e.g. 0x0104), or remove the bootloader: block."
209 if upgrade_class ==
"unknown":
211 "lr1121_firmware_update.bootloader: is configured, but source: targets an unrecognized firmware "
212 "version (0x%04X); the bootloader-rewrite path will be inert at runtime until this build's "
213 "compatibility table is extended for it (see lr1121_firmware_decisions.h)",
217 parent_id = config[CONF_ID]
218 base = parent_id.id
if parent_id.id
else "home_io_control"
219 config[CONF_LR1121_BOOTLOADER_SWITCH_ID] = ID(
220 f
"{base}_lr1121_bootloader_switch",
222 type=IOHomeLr1121BootloaderRewriteSwitch,
227def _validate_lr1121_firmware_update(config):
228 """Gate + inject the button ID for the optional lr1121_firmware_update: block.
230 Only runs when the block is present. Rejects configurations that can't reach the LR1121
231 bootloader at all (wrong radio_type, missing busy_pin) or that would silently invert the
232 bootloader-entry level (busy_pin inverted: true — bootloader entry drives BUSY to a physical
233 LOW; see radio_lr1121_firmware_updater.h). Also injects the flash button's companion ID at
234 validation time — see CONF_ACCEPT_FOREIGN_PAIRING_SWITCH_ID's comment above for why that
235 can't wait until to_code(). The bootloader:-specific checks (C3-C5, and the companion arming
236 switch's ID) live in _validate_lr1121_bootloader_block() above, called at the end of this
237 function so config[CONF_ID] and the reachability checks are already settled.
239 if CONF_LR1121_FIRMWARE_UPDATE
not in config:
242 lr1121_firmware.validate_bootloader_reachability(
243 radio_type=config[CONF_RADIO_TYPE],
244 has_busy_pin=CONF_BUSY_PIN
in config,
245 busy_pin_inverted=config.get(CONF_BUSY_PIN, {}).get(CONF_INVERTED,
False),
248 raise cv.Invalid(str(err))
from err
250 parent_id = config[CONF_ID]
251 base = parent_id.id
if parent_id.id
else "home_io_control"
252 config[CONF_LR1121_FIRMWARE_UPDATE_BUTTON_ID] = ID(
253 f
"{base}_lr1121_firmware_update_button",
255 type=IOHomeLr1121FirmwareUpdateButton,
257 return _validate_lr1121_bootloader_block(config)
265RADIO_TYPE_OPTIONS = {
271TCXO_VOLTAGE_OPTIONS = {
282DEVICE_TYPE_OPTIONS = {
284 "venetian_blind": 0x01,
285 "roller_shutter": 0x02,
287 "window_opener": 0x04,
288 "garage_opener": 0x05,
291 "rolling_door_opener": 0x08,
295 "dual_shutter": 0x0D,
296 "heating_temperature_interface": 0x0E,
297 "on_off_switch": 0x0F,
298 "horizontal_awning": 0x10,
299 "external_venetian_blind": 0x11,
300 "louvre_blind": 0x12,
301 "curtain_track": 0x13,
302 "intrusion_alarm": 0x17,
303 "swinging_shutter": 0x18,
307def _resolve_device_type_token(token):
308 """Resolve a lowercase, stripped device-type token (name or raw int/hex string) to 0-255.
310 Single source of truth for the "named value from DEVICE_TYPE_OPTIONS, else raw integer"
311 acceptance rule shared by validate_device_type() (io_device_type) and
312 validate_linked_remote_entry() (the class:<device_type> linked-remotes form) — both accept
313 the exact same set of device-type spellings, so the lookup lives here once.
314 @raises ValueError if token is neither a known name nor a parseable integer.
315 @raises cv.Invalid if token parses as an integer but is out of range 0-255.
317 if token
in DEVICE_TYPE_OPTIONS:
318 return DEVICE_TYPE_OPTIONS[token]
319 return cv.int_range(min=0, max=0xFF)(int(token, 0))
322def validate_device_type(value):
323 """Validate io_device_type as a named string or integer 0-255."""
324 if isinstance(value, int):
325 return cv.int_range(min=0, max=0xFF)(value)
327 if isinstance(value, str):
328 normalized = cv.string_strict(value).strip().lower()
330 return _resolve_device_type_token(normalized)
331 except ValueError
as err:
333 "Device type must be a known name or an integer in the range 0..255 (for example 0x11)"
337 "Device type must be a known name or an integer in the range 0..255"
341def device_type_expression(value):
342 """Generate a C++ static_cast expression for a validated device type."""
343 return cg.RawExpression(
344 f
"static_cast<esphome::home_io_control::DeviceType>(0x{value:02X})"
348def validate_node_id(value):
349 """Validate node_id as exactly 6 hex characters (3 bytes)."""
350 value = cv.string_strict(value).upper()
352 raise cv.Invalid(
"Node ID must be exactly 6 hex characters (3 bytes)")
356 raise cv.Invalid(
"Node ID must be valid hexadecimal")
360def validate_system_key(value):
361 """Validate system_key as exactly 32 hex characters (16 bytes)."""
362 value = cv.string_strict(value).upper()
364 raise cv.Invalid(
"System key must be exactly 32 hex characters (16 bytes)")
368 raise cv.Invalid(
"System key must be valid hexadecimal")
372def validate_device_id(value):
373 """Validate io_device_id as exactly 6 hex characters (3 bytes)."""
374 value = cv.string_strict(value).upper()
376 raise cv.Invalid(
"Device ID must be exactly 6 hex characters (3 bytes)")
380 raise cv.Invalid(
"Device ID must be valid hexadecimal")
384def validate_linked_remote_entry(value):
385 """Validate a linked_remotes entry: either a device ID or 'class:<device_type>'.
387 The class form matches how 1W remotes address a typed broadcast (e.g. "all awnings")
388 rather than a single node, so one entry can cover many same-type devices without
389 enumerating each one. Shares _resolve_device_type_token() with validate_device_type()
390 so a type without a named YAML alias yet (e.g. discovered via pairing) can still be
391 class-linked. Normalized to 'class:0x<HH>' (uppercase hex) so wire_device_binding() can
392 parse the type directly without a second DEVICE_TYPE_OPTIONS lookup; bare device IDs are
393 validated exactly as before and behave identically.
395 if isinstance(value, str)
and value.lower().startswith(
"class:"):
396 type_token = value.split(
":", 1)[1].strip().lower()
398 type_value = _resolve_device_type_token(type_token)
399 except ValueError
as err:
401 f
"Unknown device class '{type_token}' in linked_remotes; expected one of: "
402 +
", ".join(sorted(DEVICE_TYPE_OPTIONS))
403 +
", or a raw integer such as 0x14"
405 return f
"class:0x{type_value:02X}"
406 return validate_device_id(value)
409def validate_status_poll_interval(value):
410 """Validate status_poll_interval is at least MIN_STATUS_POLL_INTERVAL_MS."""
411 value = cv.positive_time_period_milliseconds(value)
412 if value.total_milliseconds < MIN_STATUS_POLL_INTERVAL_MS:
414 f
"status_poll_interval must be at least {MIN_STATUS_POLL_INTERVAL_MS}ms"
419CONFIG_SCHEMA = cv.All(
422 cv.GenerateID(): cv.declare_id(IOHomeControlComponent),
423 cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
424 cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
425 cv.Optional(CONF_DIO4_PIN): pins.internal_gpio_input_pin_schema,
427 cv.Optional(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
428 cv.Optional(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
429 cv.Required(CONF_NODE_ID): validate_node_id,
430 cv.Required(CONF_SYSTEM_KEY): cv.sensitive(validate_system_key),
431 cv.Optional(CONF_TX_POWER, default=17): cv.int_range(min=0, max=22),
432 cv.Optional(CONF_PA_PIN, default=
"BOOST"): cv.enum(
433 PA_PIN_OPTIONS, upper=
True
435 cv.Required(CONF_RADIO_TYPE): cv.enum(RADIO_TYPE_OPTIONS, lower=
True),
436 cv.Optional(CONF_FEM_EN_PIN): pins.internal_gpio_output_pin_schema,
437 cv.Optional(CONF_VFEM_PIN): pins.internal_gpio_output_pin_schema,
438 cv.Optional(CONF_FEM_PA_PIN): pins.internal_gpio_output_pin_schema,
439 cv.Optional(CONF_TCXO_VOLTAGE, default=
"1_8V"): cv.enum(
440 TCXO_VOLTAGE_OPTIONS, upper=
True
442 cv.Optional(CONF_EXPOSED_SENDERS, default=[]): cv.ensure_list(
445 cv.Optional(CONF_ACCEPT_FOREIGN_PAIRING, default=
False): cv.boolean,
446 cv.Optional(CONF_LR1121_FIRMWARE_UPDATE): LR1121_FIRMWARE_UPDATE_SCHEMA,
447 cv.Optional(tuning_module.CONF_TUNING): tuning_module.TUNING_CONFIG_SCHEMA,
450 .extend(cv.COMPONENT_SCHEMA)
451 .extend(spi.spi_device_schema(
True, 8e6,
"mode0")),
452 _inject_accept_foreign_pairing_switch_id,
453 _validate_lr1121_firmware_update,
457async def to_code(config):
463 cg.add_define(
"USE_API_USER_DEFINED_ACTIONS")
464 cg.add_define(
"USE_API_CUSTOM_SERVICES")
465 cg.add_define(
"USE_API_HOMEASSISTANT_SERVICES")
467 var = cg.new_Pvariable(config[CONF_ID])
468 await cg.register_component(var, config)
469 await spi.register_spi_device(var, config)
471 rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN])
472 cg.add(var.set_rst_pin(rst_pin))
474 if CONF_DIO0_PIN
in config:
475 dio0_pin = await cg.gpio_pin_expression(config[CONF_DIO0_PIN])
476 cg.add(var.set_dio0_pin(dio0_pin))
478 if CONF_DIO4_PIN
in config:
479 dio4_pin = await cg.gpio_pin_expression(config[CONF_DIO4_PIN])
480 cg.add(var.set_dio4_pin(dio4_pin))
482 if CONF_DIO1_PIN
in config:
483 dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN])
484 cg.add(var.set_dio1_pin(dio1_pin))
486 if CONF_BUSY_PIN
in config:
487 busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
488 cg.add(var.set_busy_pin(busy_pin))
490 if CONF_FEM_EN_PIN
in config:
491 fem_en_pin = await cg.gpio_pin_expression(config[CONF_FEM_EN_PIN])
492 cg.add(var.set_fem_en_pin(fem_en_pin))
494 if CONF_VFEM_PIN
in config:
495 vfem_pin = await cg.gpio_pin_expression(config[CONF_VFEM_PIN])
496 cg.add(var.set_vfem_pin(vfem_pin))
498 if CONF_FEM_PA_PIN
in config:
499 fem_pa_pin = await cg.gpio_pin_expression(config[CONF_FEM_PA_PIN])
500 cg.add(var.set_fem_pa_pin(fem_pa_pin))
502 cg.add(var.set_node_id(config[CONF_NODE_ID]))
503 cg.add(var.set_system_key(config[CONF_SYSTEM_KEY]))
504 cg.add(var.set_tx_power(config[CONF_TX_POWER]))
505 cg.add(var.set_pa_pin(config[CONF_PA_PIN]))
507 cg.add(var.set_radio_type(config[CONF_RADIO_TYPE]))
509 cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE]))
511 for sender_id
in config[CONF_EXPOSED_SENDERS]:
512 cg.add(var.add_exposed_sender(sender_id))
514 if config[CONF_ACCEPT_FOREIGN_PAIRING]:
515 await _create_accept_foreign_pairing_switch(config, var)
517 if CONF_LR1121_FIRMWARE_UPDATE
in config:
518 await _create_lr1121_firmware_update(config, var)
520 if tuning_module.CONF_TUNING
in config:
521 await tuning_module.to_code(config[tuning_module.CONF_TUNING], var)
524async def _create_accept_foreign_pairing_switch(config, var):
525 """Create the hub-level "Accept Foreign Pairing (Key Extraction)" switch.
527 Mirrors tuning.py's _create_number()/_create_select(): normalize a bare {id, name} dict
528 through switch_schema()+COMPONENT_SCHEMA so it carries the entity/component defaults
529 register_switch()/register_component() require, matching the neighboring pattern rather than
530 hand-assembling a config dict shape of its own.
532 entity_config = switch_component.switch_schema(
533 IOHomeAcceptForeignPairingSwitch,
534 default_restore_mode=
"ALWAYS_OFF",
535 entity_category=ENTITY_CATEGORY_CONFIG,
536 ).extend(cv.COMPONENT_SCHEMA)(
538 CONF_ID: config[CONF_ACCEPT_FOREIGN_PAIRING_SWITCH_ID],
539 CONF_NAME:
"Accept Foreign Pairing (Key Extraction)",
542 entity = await switch_component.new_switch(entity_config)
543 await cg.register_component(entity, entity_config)
544 cg.add(entity.set_parent(var))
547def _cached_http_fetch(cache_dir):
548 """Build a `fetch(url, expected_hash=None) -> bytes` callable for
549 lr1121_firmware.fetch_and_verify(), backed by an on-disk cache so repeat and offline builds
550 don't re-download the same source.
552 The cache key incorporates `expected_hash` (the MD5 fetch_and_verify() already resolved from
553 the `.md5` sidecar or `checksum_md5:` before calling this for the `.bin`) rather than being
554 `sha256(url)` alone. With the default `ref: HEAD` the URL never changes, so a plain
555 url-only key means a corrupt/truncated download poisons the cache permanently -- no config
556 change can ever invalidate it, since nothing about the request changes on retry. Folding the
557 expected hash in means correcting a wrong `checksum_md5:` (or a fixed upstream sidecar) misses
558 the poisoned entry and forces a fresh download. The `.md5` sidecar fetch itself has no
559 expected_hash to key on (chicken-and-egg -- it's what supplies one for the .bin) and is cached
560 under the URL alone; a corrupted sidecar is a much smaller/rarer risk than a corrupted 64+ KB
561 binary, and the cache directory below is a manual escape hatch either way.
563 Data that fails its own hash check is deliberately never written to the cache (verify-before-store):
564 a transient network corruption then simply retries cleanly on the next build, with no
565 config change needed at all.
567 cache_dir.mkdir(parents=
True, exist_ok=
True)
569 def fetch(url, expected_hash=None):
570 cache_key = hashlib.sha256(f
"{url}|{expected_hash or ''}".encode(
"utf-8")).hexdigest()
571 cache_path = cache_dir / cache_key
572 if cache_path.exists():
573 return cache_path.read_bytes()
575 with urllib.request.urlopen(url, timeout=30)
as response:
576 data = response.read()
577 except urllib.error.HTTPError
as err:
581 except urllib.error.URLError
as err:
583 if expected_hash
is None or hashlib.md5(data).hexdigest() == expected_hash:
584 cache_path.write_bytes(data)
590def _render_lr1121_image_header(image, array_name, words_name, version_name):
591 """Render a verified firmware/loader image as a C++ header.
593 Each raw 4-byte chunk of the `.bin` is exactly one big-endian word as Semtech's own image
594 format already lays it out, so this only has to slice and format, not transform, the bytes.
595 `inline const` (not `constexpr`) for the array: it is never used in a constant expression, so
596 forcing constant-evaluation of up to ~61k elements would only cost compile time; `const` at
597 namespace scope still lands in `.rodata` (flash) on ESP32, not RAM. Shared by
598 _render_lr1121_firmware_header() (the transceiver image) and the bootloader loader image --
599 same shape, different symbol names so both headers can be included from the same translation
600 unit without colliding.
602 words = [f
"0x{int.from_bytes(image.data[i : i + 4], 'big'):08X}" for i
in range(0, len(image.data), 4)]
605 " " +
", ".join(words[i : i + words_per_line]) +
"," for i
in range(0, len(words), words_per_line)
610 "// Auto-generated by the home_io_control lr1121_firmware_update build step. Do not edit.",
611 "#include <cstddef>",
612 "#include <cstdint>",
614 "namespace esphome {",
615 "namespace home_io_control {",
617 f
"inline const uint32_t {array_name}[] = {{",
620 f
"inline constexpr size_t {words_name} = {len(words)};",
621 f
"inline constexpr uint16_t {version_name} = 0x{image.version:04X};",
623 "} // namespace home_io_control",
624 "} // namespace esphome",
630def _render_lr1121_firmware_header(image):
631 """Render the verified transceiver firmware image as a C++ header."""
632 return _render_lr1121_image_header(
633 image,
"LR1121_FIRMWARE_UPDATE_IMAGE",
"LR1121_FIRMWARE_UPDATE_IMAGE_WORDS",
"LR1121_FIRMWARE_UPDATE_TARGET_VERSION"
637def _render_lr1121_bootloader_loader_header(image):
638 """Render the verified bootloader *loader* image as a C++ header (ADR 0021)."""
639 return _render_lr1121_image_header(
640 image,
"LR1121_BOOTLOADER_LOADER_IMAGE",
"LR1121_BOOTLOADER_LOADER_IMAGE_WORDS",
"LR1121_BOOTLOADER_LOADER_FW"
644async def _create_lr1121_firmware_update(config, var):
645 """Fetch/verify the configured firmware image, generate its header, set the build flag that
646 gates the whole feature, and create the "Flash LR1121 Radio Firmware" button.
648 The block's mere presence in YAML is the build flag (ADR 0020) — there is no
649 separate enable switch, so entering/leaving flash mode is a recompile + OTA each way.
651 fw_config = config[CONF_LR1121_FIRMWARE_UPDATE]
652 cache_dir = CORE.data_dir /
"lr1121_firmware_cache"
654 image = lr1121_firmware.fetch_and_verify(
655 source=fw_config[CONF_SOURCE],
656 ref=fw_config.get(CONF_REF),
657 checksum_md5=fw_config.get(CONF_CHECKSUM_MD5),
658 target_version=fw_config.get(CONF_TARGET_VERSION),
659 fetch=_cached_http_fetch(cache_dir),
662 raise cv.Invalid(f
"lr1121_firmware_update: {err}")
from err
664 header_path = CORE.relative_src_path(
"lr1121_firmware_update_image.h")
665 write_file_if_changed(header_path, _render_lr1121_firmware_header(image))
667 cg.add_define(
"IOHOME_LR1121_FIRMWARE_UPDATE")
669 if CONF_LR1121_BOOTLOADER
in fw_config:
670 await _create_lr1121_bootloader_update(fw_config[CONF_LR1121_BOOTLOADER], config, var, cache_dir)
672 entity_config = button_component.button_schema(
673 IOHomeLr1121FirmwareUpdateButton,
674 entity_category=ENTITY_CATEGORY_CONFIG,
675 ).extend(cv.COMPONENT_SCHEMA)(
677 CONF_ID: config[CONF_LR1121_FIRMWARE_UPDATE_BUTTON_ID],
678 CONF_NAME:
"Flash LR1121 Radio Firmware",
681 entity = await button_component.new_button(entity_config)
682 await cg.register_component(entity, entity_config)
683 cg.add(entity.set_parent(var))
686async def _create_lr1121_bootloader_update(bootloader_config, config, var, cache_dir):
687 """Fetch/verify the configured loader image, generate its header, set the build flag that
688 gates the bootloader-rewrite feature, and create the arming switch.
690 Mirrors _create_lr1121_firmware_update() above -- same "block's presence is the build flag"
691 shape, one level down (ADR 0021). `target_version` is not passed to
692 fetch_and_verify(): the loader is not a "target" the way the transceiver image is, its version
693 is only ever compared for *equality* against the currently-running bootloader (Semtech's
694 rule), so there is nothing to override.
697 loader_image = lr1121_firmware.fetch_and_verify(
698 source=bootloader_config[CONF_SOURCE],
699 ref=bootloader_config.get(CONF_REF),
700 checksum_md5=bootloader_config.get(CONF_CHECKSUM_MD5),
702 fetch=_cached_http_fetch(cache_dir),
705 raise cv.Invalid(f
"lr1121_firmware_update.bootloader: {err}")
from err
707 header_path = CORE.relative_src_path(
"lr1121_bootloader_loader_image.h")
708 write_file_if_changed(header_path, _render_lr1121_bootloader_loader_header(loader_image))
710 cg.add_define(
"IOHOME_LR1121_BOOTLOADER_UPDATE")
712 entity_config = switch_component.switch_schema(
713 IOHomeLr1121BootloaderRewriteSwitch,
714 default_restore_mode=
"ALWAYS_OFF",
715 entity_category=ENTITY_CATEGORY_CONFIG,
716 ).extend(cv.COMPONENT_SCHEMA)(
718 CONF_ID: config[CONF_LR1121_BOOTLOADER_SWITCH_ID],
719 CONF_NAME:
"Allow LR1121 Bootloader Rewrite (Irreversible)",
732 entity = await switch_component.new_switch(entity_config)
733 await cg.register_component(entity, entity_config)
734 cg.add(entity.set_parent(var))
validate_checksum_md5(value)
_inject_accept_foreign_pairing_switch_id(config)
validate_lr1121_firmware_source(value, *, expect_loader=False)