Files
esphome/esphome/components/ota/__init__.py
T

195 lines
7.0 KiB
Python

from esphome import automation
import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
CONF_ON_ERROR,
CONF_OTA,
CONF_PLATFORM,
CONF_TRIGGER_ID,
PlatformFramework,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
OTA_STATE_LISTENER_KEY = "ota_state_listener"
CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD() -> list[str]:
components = ["safe_mode"]
if not CORE.using_zephyr:
components.extend(["md5"])
if CORE.is_esp32:
components.extend(["watchdog"])
return components
IS_PLATFORM_COMPONENT = True
CONF_ON_ABORT = "on_abort"
CONF_ON_BEGIN = "on_begin"
CONF_ON_END = "on_end"
CONF_ON_PROGRESS = "on_progress"
CONF_ON_STATE_CHANGE = "on_state_change"
ota_ns = cg.esphome_ns.namespace("ota")
OTAComponent = ota_ns.class_("OTAComponent", cg.Component)
OTAState = ota_ns.enum("OTAState")
OTAAbortTrigger = ota_ns.class_("OTAAbortTrigger", automation.Trigger.template())
OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template())
OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template())
OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template())
OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template())
OTAStateChangeTrigger = ota_ns.class_(
"OTAStateChangeTrigger", automation.Trigger.template()
)
def _ota_final_validate(config: ConfigType) -> None:
if len(config) < 1:
raise cv.Invalid(
f"At least one platform must be specified for '{CONF_OTA}'; add '{CONF_PLATFORM}: {CONF_ESPHOME}' for original OTA functionality"
)
FINAL_VALIDATE_SCHEMA = _ota_final_validate
BASE_OTA_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStateChangeTrigger),
}
),
cv.Optional(CONF_ON_ABORT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAAbortTrigger),
}
),
cv.Optional(CONF_ON_BEGIN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStartTrigger),
}
),
cv.Optional(CONF_ON_END): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAEndTrigger),
}
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAErrorTrigger),
}
),
cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAProgressTrigger),
}
),
}
)
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_OTA")
CORE.add_job(final_step)
if CORE.is_rp2 and CORE.using_arduino:
cg.add_library("Updater", None)
async def ota_to_code(var: MockObj, config: ConfigType) -> None:
await cg.past_safe_mode()
use_state_callback = False
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(OTAState, "state")], conf)
use_state_callback = True
for conf in config.get(CONF_ON_ABORT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_BEGIN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_PROGRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(float, "x")], conf)
use_state_callback = True
for conf in config.get(CONF_ON_END, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_ERROR, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
use_state_callback = True
if use_state_callback:
request_ota_state_listeners()
def request_ota_state_listeners() -> None:
"""Request that OTA state listeners be compiled in.
Components that need to be notified about OTA state changes (start, progress,
complete, error) should call this function during their code generation.
This enables the add_state_listener() API on OTAComponent.
"""
CORE.data[OTA_STATE_LISTENER_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step() -> None:
"""Final code generation step to configure optional OTA features."""
if CORE.data.get(OTA_STATE_LISTENER_KEY, False):
cg.add_define("USE_OTA_STATE_LISTENER")
_filter_backend_source_files = filter_source_files_from_platform(
{
"ota_backend_esp_idf.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"ota_backend_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"ota_backend_arduino_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
"ota_backend_arduino_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"ota_backend_host.cpp": {PlatformFramework.HOST_NATIVE},
}
)
def FILTER_SOURCE_FILES() -> list[str]:
files = _filter_backend_source_files()
# ota_signature_esp_idf.cpp implements multi-key OTA signature verification,
# compiled only when the esp32 component enables it (external RSA signed
# OTA sets USE_OTA_SIGNED_VERIFICATION_MULTI_KEY). The define is set only on
# ESP32/IDF, so this also excludes the file on every other platform. Filter
# it out otherwise so the (otherwise fully #ifdef'd-out) file isn't opened
# and parsed on every build.
if not any(
define.name == "USE_OTA_SIGNED_VERIFICATION_MULTI_KEY"
for define in CORE.defines
):
files.append("ota_signature_esp_idf.cpp")
# ota_bootloader_esp_idf.cpp and ota_partitions_esp_idf.cpp are fully
# #ifdef'd on USE_OTA_PARTITIONS (set by the esphome OTA platform when
# allow_partition_access is enabled). Filter them out otherwise for the
# same reason as above.
if not any(define.name == "USE_OTA_PARTITIONS" for define in CORE.defines):
files.append("ota_bootloader_esp_idf.cpp")
files.append("ota_partitions_esp_idf.cpp")
return files