mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4743029c7 | ||
|
|
991768c7f4 | ||
|
|
32c835f43f | ||
|
|
279f2ba5b8 | ||
|
|
516c93b001 | ||
|
|
c68e7e21fc | ||
|
|
dbdd0fda72 | ||
|
|
e2bbbb1383 | ||
|
|
d1f065671e | ||
|
|
02da5c6484 | ||
|
|
b2440cb655 | ||
|
|
160d8b8f0c | ||
|
|
8899713ef9 | ||
|
|
f3cdefce21 | ||
|
|
b115813fbe | ||
|
|
ab45ab316a | ||
|
|
5b3a6c05bf | ||
|
|
cd53681787 | ||
|
|
f0651e5c9b | ||
|
|
f24b731f95 | ||
|
|
01ad424d12 | ||
|
|
435d522683 | ||
|
|
a63c3bc0c7 | ||
|
|
b83ce91528 | ||
|
|
a30238aab6 | ||
|
|
47156c9a5b | ||
|
|
78240c9a46 | ||
|
|
5a9f06e584 | ||
|
|
ecb007da70 | ||
|
|
ad1a4fca36 | ||
|
|
832a738588 | ||
|
|
fedb3ac5c1 | ||
|
|
cb4e55e444 | ||
|
|
3ef5a8e6a4 | ||
|
|
0e915e9b8b | ||
|
|
dba3b287dd | ||
|
|
ce019f508d | ||
|
|
5c2286cc4a | ||
|
|
efc0a94112 | ||
|
|
c60062c418 | ||
|
|
763a1d9371 | ||
|
|
4db1666024 | ||
|
|
0dc69aab1e | ||
|
|
a282cb095e | ||
|
|
259e7182a3 | ||
|
|
74bdf275d2 | ||
|
|
14499223fd | ||
|
|
6aab523dd9 | ||
|
|
1f31e51446 | ||
|
|
c062d0c717 | ||
|
|
dcabaedff1 | ||
|
|
d119ad6c60 | ||
|
|
74fc2e367a | ||
|
|
ea10f94376 | ||
|
|
ef1d77885d | ||
|
|
dccf55eadc | ||
|
|
65704e881f | ||
|
|
a30e82459f | ||
|
|
5a300e92f1 | ||
|
|
8e9fb0f93c |
@@ -381,6 +381,7 @@ esphome/components/nextion/switch/* @senexcrenshaw
|
||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
|
||||
esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/noise/* @esphome/core
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/number/* @esphome/core
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.3
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+30
-3
@@ -26,7 +26,9 @@ from esphome.const import (
|
||||
CONF_DEASSERT_RTS_DTR,
|
||||
CONF_DISABLED,
|
||||
CONF_DISCOVER_IP,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_ESPHOME,
|
||||
CONF_KEY,
|
||||
CONF_LEVEL,
|
||||
CONF_LOG,
|
||||
CONF_LOG_TOPIC,
|
||||
@@ -762,9 +764,11 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
async def wrapped(conf):
|
||||
cg.add(cg.LineComment(f"{name}:"))
|
||||
if comp.config_schema is not None:
|
||||
conf_str = yaml_util.dump(conf)
|
||||
# sort_keys: voluptuous fills defaults in set order, so an
|
||||
# unsorted dump would churn main.cpp and relink every run
|
||||
conf_str = yaml_util.dump(conf, sort_keys=True)
|
||||
conf_str = conf_str.replace("//", "")
|
||||
# remove tailing \ to avoid multi-line comment warning
|
||||
# remove trailing \ to avoid multi-line comment warning
|
||||
conf_str = conf_str.replace("\\\n", "\n")
|
||||
cg.add(cg.LineComment(indent(conf_str)))
|
||||
await coro(conf)
|
||||
@@ -1321,6 +1325,17 @@ def _upload_via_native_api(
|
||||
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD)
|
||||
# Final validate resolved a bare `encryption:` block to the api key.
|
||||
# Fail closed: if the block is present but no key was resolved, never
|
||||
# fall back to a plaintext upload of an image that carries credentials.
|
||||
noise_psk = None
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
noise_psk = encryption_conf.get(CONF_KEY)
|
||||
if not noise_psk:
|
||||
raise EsphomeError(
|
||||
"OTA encryption is configured but no key was resolved; "
|
||||
"set the key under 'ota: encryption:' or 'api: encryption:'"
|
||||
)
|
||||
|
||||
def check_partition_access(option_string: str) -> None:
|
||||
if not ota_conf.get("allow_partition_access"):
|
||||
@@ -1351,7 +1366,9 @@ def _upload_via_native_api(
|
||||
if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER:
|
||||
_validate_bootloader_binary(binary)
|
||||
|
||||
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
|
||||
return espota2.run_ota(
|
||||
network_devices, remote_port, password, binary, ota_type, noise_psk
|
||||
)
|
||||
|
||||
|
||||
def _upload_via_web_server(
|
||||
@@ -1360,6 +1377,16 @@ def _upload_via_web_server(
|
||||
from esphome import web_server_ota
|
||||
from esphome.web_server_helpers import get_web_server_connection
|
||||
|
||||
if any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
and ota_item.get(CONF_ENCRYPTION) is not None
|
||||
for ota_item in config.get(CONF_OTA, [])
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"This config has OTA encryption, but the web_server OTA path sends "
|
||||
"the image over plaintext HTTP; use the esphome OTA platform to "
|
||||
"keep it confidential"
|
||||
)
|
||||
remote_port, username, password = get_web_server_connection(config)
|
||||
return web_server_ota.run_ota(
|
||||
network_devices, remote_port, username, password, binary
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
@@ -16,6 +18,7 @@ from esphome.components.esp32 import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
@@ -225,7 +228,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
}
|
||||
|
||||
|
||||
def validate_adc_pin(value):
|
||||
def validate_adc_pin(value: Any) -> ConfigType | str:
|
||||
if str(value).upper() == "VCC":
|
||||
if CORE.is_rp2:
|
||||
return pins.internal_gpio_input_pin_schema(29)
|
||||
|
||||
@@ -52,7 +52,7 @@ _attenuation = cv.enum(ATTENUATION_MODES, lower=True)
|
||||
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
|
||||
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
|
||||
|
||||
@@ -120,7 +120,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
|
||||
def _overlay_io_channels():
|
||||
def _overlay_io_channels() -> str:
|
||||
channel_count = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count))
|
||||
return f"""
|
||||
@@ -132,7 +132,7 @@ def _overlay_io_channels():
|
||||
"""
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.logger import request_log_listener
|
||||
|
||||
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
|
||||
# components and downstream consumers that import them from api
|
||||
from esphome.components.noise import ( # noqa: F401
|
||||
ENCRYPTION_SCHEMA,
|
||||
decode_encryption_key,
|
||||
encryption_schema,
|
||||
validate_encryption_key,
|
||||
)
|
||||
from esphome.config_helpers import get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -37,6 +46,10 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
# Compat alias: downstream consumers (e.g. device-builder) referenced the
|
||||
# schema by its old private name before it moved to the noise component
|
||||
_encryption_schema = encryption_schema
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "api"
|
||||
@@ -45,9 +58,15 @@ CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
"""Conditionally auto-load json only when capture_response is used."""
|
||||
"""Conditionally auto-load noise (encryption) and json (capture_response)."""
|
||||
base = ["socket"]
|
||||
|
||||
# A falsy config is a tooling probe for the maximal set (None from
|
||||
# dependency resolution, {} from the components-graph platform probe);
|
||||
# a validated config always carries defaults, never empty
|
||||
if not config or CONF_ENCRYPTION in config:
|
||||
base = base + ["noise"]
|
||||
|
||||
# Check if any homeassistant.action/homeassistant.service has capture_response: true
|
||||
# This flag is set during config validation in _validate_response_config
|
||||
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
|
||||
@@ -129,20 +148,6 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
|
||||
# Return original data for roundtrip conversion
|
||||
return value
|
||||
|
||||
|
||||
CONF_SUPPORTS_RESPONSE = "supports_response"
|
||||
|
||||
# Enum values in api::enums namespace
|
||||
@@ -217,7 +222,7 @@ def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_supports_response(value):
|
||||
def _validate_supports_response(value: Any) -> str:
|
||||
"""Validate supports_response after auto-detection has set the value."""
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
@@ -249,18 +254,6 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
),
|
||||
)
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _encryption_schema(config):
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for API component."""
|
||||
@@ -296,7 +289,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
@@ -393,7 +386,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
triggers: list[cg.MockObj] = []
|
||||
for conf in actions:
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
@@ -483,7 +476,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
decoded = base64.b64decode(key)
|
||||
decoded = decode_encryption_key(key)
|
||||
cg.add(var.set_noise_psk(list(decoded)))
|
||||
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
|
||||
else:
|
||||
@@ -497,10 +490,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.21")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
@@ -581,7 +570,7 @@ async def homeassistant_service_to_code(
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
):
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
@@ -647,7 +636,7 @@ async def homeassistant_service_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def validate_homeassistant_event(value):
|
||||
def validate_homeassistant_event(value: Any) -> str:
|
||||
value = cv.string(value)
|
||||
if not value.startswith("esphome."):
|
||||
raise cv.Invalid(
|
||||
@@ -676,7 +665,12 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
async def homeassistant_event_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -724,7 +718,12 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
||||
async def homeassistant_tag_scanned_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -740,7 +739,7 @@ CONF_SUCCESS = "success"
|
||||
CONF_ERROR_MESSAGE = "error_message"
|
||||
|
||||
|
||||
def _validate_api_respond_data(config):
|
||||
def _validate_api_respond_data(config: ConfigType) -> ConfigType:
|
||||
"""Set flag during validation so AUTO_LOAD can include json component."""
|
||||
if CONF_DATA in config:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||
@@ -824,7 +823,12 @@ API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
@automation.register_condition(
|
||||
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
|
||||
)
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
async def api_connected_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
|
||||
@@ -2130,7 +2130,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
#endif
|
||||
|
||||
psk_t psk{};
|
||||
noise::psk_t psk{};
|
||||
if (msg.key_len == 0) {
|
||||
if (this->parent_->clear_noise_psk(true)) {
|
||||
resp.success = true;
|
||||
@@ -2139,7 +2139,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (APINoiseContext::is_all_zeros(psk)) {
|
||||
} else if (noise::NoiseContext::is_all_zeros(psk)) {
|
||||
// Accepting the reserved provisioning PSK would report success without
|
||||
// enabling encryption (or silently clear an existing key)
|
||||
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
@@ -17,6 +17,14 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
using noise::noise_err_to_logstr;
|
||||
|
||||
// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is
|
||||
// also compiled in plaintext-only builds without the noise component; keep
|
||||
// the two definitions from drifting apart.
|
||||
static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
|
||||
"api and noise component handshake size limits must match");
|
||||
|
||||
static const char *const TAG = "api.noise";
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
||||
@@ -51,45 +59,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
const LogString *noise_err_to_logstr(int err) {
|
||||
if (err == NOISE_ERROR_NO_MEMORY)
|
||||
return LOG_STR("NO_MEMORY");
|
||||
if (err == NOISE_ERROR_UNKNOWN_ID)
|
||||
return LOG_STR("UNKNOWN_ID");
|
||||
if (err == NOISE_ERROR_UNKNOWN_NAME)
|
||||
return LOG_STR("UNKNOWN_NAME");
|
||||
if (err == NOISE_ERROR_MAC_FAILURE)
|
||||
return LOG_STR("MAC_FAILURE");
|
||||
if (err == NOISE_ERROR_NOT_APPLICABLE)
|
||||
return LOG_STR("NOT_APPLICABLE");
|
||||
if (err == NOISE_ERROR_SYSTEM)
|
||||
return LOG_STR("SYSTEM");
|
||||
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
|
||||
return LOG_STR("REMOTE_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
|
||||
return LOG_STR("LOCAL_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_PSK_REQUIRED)
|
||||
return LOG_STR("PSK_REQUIRED");
|
||||
if (err == NOISE_ERROR_INVALID_LENGTH)
|
||||
return LOG_STR("INVALID_LENGTH");
|
||||
if (err == NOISE_ERROR_INVALID_PARAM)
|
||||
return LOG_STR("INVALID_PARAM");
|
||||
if (err == NOISE_ERROR_INVALID_STATE)
|
||||
return LOG_STR("INVALID_STATE");
|
||||
if (err == NOISE_ERROR_INVALID_NONCE)
|
||||
return LOG_STR("INVALID_NONCE");
|
||||
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
|
||||
return LOG_STR("INVALID_PRIVATE_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
|
||||
return LOG_STR("INVALID_PUBLIC_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_FORMAT)
|
||||
return LOG_STR("INVALID_FORMAT");
|
||||
if (err == NOISE_ERROR_INVALID_SIGNATURE)
|
||||
return LOG_STR("INVALID_SIGNATURE");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
APIError APINoiseFrameHelper::init() {
|
||||
APIError err = init_common_();
|
||||
@@ -194,9 +163,9 @@ APIError APINoiseFrameHelper::loop() {
|
||||
*/
|
||||
APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
// read header
|
||||
if (rx_header_buf_len_ < 3) {
|
||||
if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
|
||||
// no header information yet
|
||||
uint8_t to_read = 3 - rx_header_buf_len_;
|
||||
uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
|
||||
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
|
||||
APIError err = handle_socket_read_result_(received);
|
||||
if (err != APIError::OK) {
|
||||
@@ -208,7 +177,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (rx_header_buf_[0] != 0x01) {
|
||||
if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||
return APIError::BAD_INDICATOR;
|
||||
@@ -348,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_() {
|
||||
int action = noise_handshakestate_get_action(this->handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
||||
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
|
||||
return this->state_action_handshake_read_();
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
|
||||
return this->state_action_handshake_write_();
|
||||
}
|
||||
// bad state for action
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
@@ -368,20 +337,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
if (this->rx_buf_.empty()) {
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (this->rx_buf_[0] != 0x00) {
|
||||
} else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
|
||||
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
}
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
|
||||
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
|
||||
: LOG_STR("Handshake error"));
|
||||
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
|
||||
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
@@ -390,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
uint8_t buffer[65];
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
size_t msg_len = 0;
|
||||
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
|
||||
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
|
||||
APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
buffer[0] = 0x00; // success
|
||||
buffer[0] = noise::HANDSHAKE_STATUS_OK;
|
||||
|
||||
aerr = this->write_frame_(buffer, mbuf.size + 1);
|
||||
aerr = this->write_frame_(buffer, msg_len + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->check_handshake_finished_();
|
||||
@@ -409,32 +372,18 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
||||
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
|
||||
uint8_t data[32];
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
#else
|
||||
// Normal memory access
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(data + 1, reason_str, reason_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t data_size = reason_len + 1;
|
||||
static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE,
|
||||
"reject buffer must fit the MAC failure wire contract");
|
||||
size_t data_size = noise::format_reject_payload(data, sizeof(data), reason);
|
||||
|
||||
// temporarily remove failed state
|
||||
auto orig_state = state_;
|
||||
state_ = State::EXPLICIT_REJECT;
|
||||
write_frame_(data, data_size);
|
||||
APIError aerr = write_frame_(data, data_size);
|
||||
if (aerr != APIError::OK) {
|
||||
// Best effort; the reject reason is a diagnosis aid, not a protocol step
|
||||
HELPER_LOG("Sending handshake reject failed: %d", (int) aerr);
|
||||
}
|
||||
state_ = orig_state;
|
||||
}
|
||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
@@ -492,12 +441,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
// Returns APIError::OK on success.
|
||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out) {
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
// The noise frame header is written after encryption, when the size is known
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
|
||||
@@ -515,11 +462,10 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
// Fill in the frame header now that the encrypted size is known
|
||||
noise::write_frame_header(buf_start, static_cast<uint16_t>(mbuf.size));
|
||||
|
||||
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -568,21 +514,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
uint8_t header[3];
|
||||
header[0] = 0x01; // indicator
|
||||
header[1] = (uint8_t) (len >> 8);
|
||||
header[2] = (uint8_t) len;
|
||||
uint8_t header[noise::FRAME_HEADER_SIZE];
|
||||
noise::write_frame_header(header, len);
|
||||
|
||||
if (len == 0) {
|
||||
return this->write_raw_buf_(header, 3);
|
||||
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
|
||||
}
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = header;
|
||||
iov[0].iov_len = 3;
|
||||
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
|
||||
iov[1].iov_base = const_cast<uint8_t *>(data);
|
||||
iov[1].iov_len = len;
|
||||
|
||||
return this->write_raw_iov_(iov, 2, 3 + len);
|
||||
return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
|
||||
}
|
||||
|
||||
/** Initiate the data structures for the handshake.
|
||||
@@ -590,45 +534,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
* @return 0 on success, -1 on error (check errno)
|
||||
*/
|
||||
APIError APINoiseFrameHelper::init_handshake_() {
|
||||
int err;
|
||||
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
|
||||
// noise_handshakestate_new_by_id copies it, so a member would waste
|
||||
// 104 bytes per connection, and a static const would sit in RAM on
|
||||
// ESP8266 (.rodata is DRAM there).
|
||||
const NoiseProtocolId nid = {
|
||||
.prefix_id = NOISE_PREFIX_STANDARD,
|
||||
.pattern_id = NOISE_PATTERN_NN,
|
||||
.modifier_ids = {NOISE_MODIFIER_PSK0},
|
||||
.dh_id = NOISE_DH_CURVE25519,
|
||||
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
|
||||
.hash_id = NOISE_HASH_SHA256,
|
||||
.hybrid_id = NOISE_DH_NONE,
|
||||
};
|
||||
|
||||
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
|
||||
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
const auto &psk = this->ctx_.get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
|
||||
APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
||||
// init copies the prologue into the handshakestate, so we can get rid of it now
|
||||
prologue_.release();
|
||||
|
||||
err = noise_handshakestate_start(handshake_);
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -637,15 +548,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
assert(state_ == State::HANDSHAKE);
|
||||
#endif
|
||||
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
|
||||
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
||||
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
|
||||
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
|
||||
return APIError::OK;
|
||||
if (action != NOISE_ACTION_SPLIT) {
|
||||
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
|
||||
// split() also frees the handshake state
|
||||
int err = this->handshake_.split(send_cipher_, recv_cipher_);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
@@ -654,17 +567,11 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
|
||||
HELPER_LOG("Handshake complete!");
|
||||
noise_handshakestate_free(handshake_);
|
||||
handshake_ = nullptr;
|
||||
state_ = State::DATA;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APINoiseFrameHelper::~APINoiseFrameHelper() {
|
||||
if (handshake_ != nullptr) {
|
||||
noise_handshakestate_free(handshake_);
|
||||
handshake_ = nullptr;
|
||||
}
|
||||
if (send_cipher_ != nullptr) {
|
||||
noise_cipherstate_free(send_cipher_);
|
||||
send_cipher_ = nullptr;
|
||||
@@ -675,16 +582,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
|
||||
void noise_rand_bytes(void *output, size_t len) {
|
||||
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
|
||||
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
|
||||
arch_restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_NOISE
|
||||
#endif // USE_API
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "noise/protocol.h"
|
||||
#include "api_noise_context.h"
|
||||
#include "esphome/components/noise/noise_handshake.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
|
||||
static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len
|
||||
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
@@ -52,13 +52,13 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError handle_handshake_frame_error_(APIError aerr);
|
||||
APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
// Pointers first (4 bytes each; the handshake wrapper holds one pointer)
|
||||
noise::NoiseResponderHandshake handshake_;
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
APINoiseContext &ctx_;
|
||||
noise::NoiseContext &ctx_;
|
||||
|
||||
// Buffer for noise handshake prologue (released after handshake)
|
||||
APIBuffer prologue_;
|
||||
@@ -67,7 +67,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
// Fixed-size header buffer for noise protocol:
|
||||
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
||||
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
||||
uint8_t rx_header_buf_[3];
|
||||
uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE];
|
||||
uint8_t rx_header_buf_len_ = 0;
|
||||
// 4 bytes total, no padding
|
||||
};
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class APINoiseContext {
|
||||
public:
|
||||
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
|
||||
// doubles as the well-known provisioning PSK that unprovisioned devices
|
||||
// accept for Noise handshakes (passive-sniffing protection only, no
|
||||
// authentication). It is never a valid real key.
|
||||
static bool is_all_zeros(const psk_t &psk) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t b : psk) {
|
||||
acc |= b;
|
||||
}
|
||||
return acc == 0;
|
||||
}
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
this->has_psk_ = !is_all_zeros(psk);
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
protected:
|
||||
psk_t psk_{};
|
||||
bool has_psk_{false};
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
} // namespace esphome::api
|
||||
@@ -423,12 +423,6 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
bool has_subscriber = false;
|
||||
@@ -553,10 +547,6 @@ const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_sta
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t APIServer::get_port() const { return this->port_; }
|
||||
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, bool make_active) {
|
||||
@@ -598,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
// but if it is, reject the change
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
#include "api_buffer.h"
|
||||
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
|
||||
#include "api_connection.h"
|
||||
#include "api_noise_context.h"
|
||||
#ifdef USE_API_NOISE
|
||||
// Only present in the build when the noise component is loaded
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#endif
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_service.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -37,7 +40,7 @@ class UserServiceDescriptor;
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
psk_t psk;
|
||||
noise::psk_t psk;
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
@@ -51,8 +54,8 @@ class APIServer final : public Component,
|
||||
public:
|
||||
APIServer();
|
||||
void setup() override;
|
||||
uint16_t get_port() const;
|
||||
float get_setup_priority() const override;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
@@ -63,9 +66,9 @@ class APIServer final : public Component,
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
#endif
|
||||
void set_port(uint16_t port);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
|
||||
@@ -73,10 +76,10 @@ class APIServer final : public Component,
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
@@ -354,7 +357,7 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
APINoiseContext noise_ctx_;
|
||||
noise::NoiseContext noise_ctx_;
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
|
||||
@@ -4,9 +4,12 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack
|
||||
bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
|
||||
on this component and contain no SDK calls of their own.
|
||||
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
|
||||
(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in
|
||||
to_code; unknown families are capability-checked at compile time via
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE
|
||||
compiled in, the Beken SDK erases the bootloader flash sector at boot because
|
||||
LibreTiny's partition table has no BLE bonding entry (esphome#18646,
|
||||
libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in
|
||||
to_code. Unknown families are capability-checked at compile time via
|
||||
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
|
||||
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
|
||||
fails with a clear #error.
|
||||
@@ -65,6 +68,14 @@ def _unsupported_family_message(family: str) -> str | None:
|
||||
)
|
||||
if family == FAMILY_BK7231Q:
|
||||
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
|
||||
if family == FAMILY_BK7238:
|
||||
return (
|
||||
"bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK "
|
||||
"erases the bootloader flash sector at boot and the device can no longer "
|
||||
"start (see https://github.com/esphome/esphome/issues/18646); support "
|
||||
"returns once the LibreTiny partition table fix "
|
||||
"(libretiny-eu/libretiny#408) is released"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -113,18 +124,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
|
||||
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
|
||||
# which path is available so it doesn't reference a missing symbol.
|
||||
family = libretiny.get_libretiny_family()
|
||||
if family == FAMILY_BK7231N:
|
||||
if libretiny.get_libretiny_family() == FAMILY_BK7231N:
|
||||
cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR")
|
||||
elif family == FAMILY_BK7238:
|
||||
# ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at
|
||||
# WiFi STA startup when BLE init runs. This component re-enables BLE, so
|
||||
# warn loudly: BK7238 is accepted but not hardware-verified and may be
|
||||
# WiFi-unstable with BLE on.
|
||||
_LOGGER.warning(
|
||||
"bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup "
|
||||
"hang on this family and is not yet hardware-verified. Expect possible "
|
||||
"instability."
|
||||
)
|
||||
|
||||
cg.add_define("USE_BK72XX_BLE")
|
||||
|
||||
@@ -16,14 +16,15 @@ from esphome.const import (
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -88,7 +89,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
@setup_entity("button")
|
||||
async def setup_button_core_(var, config):
|
||||
async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
setup_device_class(config)
|
||||
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_button(var, config):
|
||||
async def register_button(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("button", config)
|
||||
@@ -109,7 +110,7 @@ async def register_button(var, config):
|
||||
await setup_button_core_(var, config)
|
||||
|
||||
|
||||
async def new_button(config, *args):
|
||||
async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_button(var, config)
|
||||
return var
|
||||
@@ -125,11 +126,16 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
|
||||
)
|
||||
async def button_press_to_code(config, action_id, template_arg, args):
|
||||
async def button_press_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(button_ns.using)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include "captive_index.h"
|
||||
#include "scan_list.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
@@ -33,8 +34,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
// Invariant: only bounded in-memory work under the lock; the network send
|
||||
// happens later in request->send()
|
||||
wifi::ScanResultsLock lock(wifi::global_wifi_component);
|
||||
for (const auto &scan : wifi::global_wifi_component->get_scan_result()) {
|
||||
if (scan.get_is_hidden())
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
for (const auto &scan : results) {
|
||||
bool with_auth = false;
|
||||
if (!should_show_scan_entry(results, scan, with_auth))
|
||||
continue;
|
||||
|
||||
json_escape_into_buffer(escaped_ssid, scan.get_ssid());
|
||||
@@ -44,10 +47,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
stream->print(ESPHOME_F("\",\"rssi\":"));
|
||||
stream->print(scan.get_rssi());
|
||||
stream->print(ESPHOME_F(",\"lock\":"));
|
||||
stream->print(scan.get_with_auth());
|
||||
stream->print(with_auth);
|
||||
stream->print(ESPHOME_F("}"));
|
||||
#else
|
||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), scan.get_with_auth());
|
||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), with_auth);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
// A scan lists every BSSID, so one SSID can appear several times. Returns true for
|
||||
// the strongest entry per SSID (earliest on ties), never for hidden entries. scan
|
||||
// must be an element of results. with_auth is written only when returning true and
|
||||
// is set if any entry with that SSID needs a key. Templated for host tests.
|
||||
template<typename Results, typename Entry>
|
||||
bool should_show_scan_entry(const Results &results, const Entry &scan, bool &with_auth) {
|
||||
if (scan.get_is_hidden())
|
||||
return false;
|
||||
const int8_t rssi = scan.get_rssi();
|
||||
bool any_auth = false;
|
||||
for (const auto &other : results) {
|
||||
if (other.get_is_hidden() || !other.ssid_equals(scan))
|
||||
continue;
|
||||
// Same array, so address order is index order. scan fails both checks against itself.
|
||||
if (other.get_rssi() > rssi || (other.get_rssi() == rssi && &other < &scan))
|
||||
return false;
|
||||
any_auth |= other.get_with_auth();
|
||||
}
|
||||
with_auth = any_auth;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
@@ -48,13 +50,19 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObjClass
|
||||
from esphome.cpp_generator import (
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
TemplateArgsType,
|
||||
)
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -132,7 +140,7 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def visual_temperature_step(value):
|
||||
def visual_temperature_step(value: Any) -> ConfigType:
|
||||
# Allow defining target/current temperature steps separately
|
||||
if isinstance(value, dict):
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
|
||||
@@ -273,7 +281,7 @@ def climate_schema(
|
||||
|
||||
|
||||
@setup_entity("climate")
|
||||
async def setup_climate_core_(var, config):
|
||||
async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
visual = config.get(CONF_VISUAL, {})
|
||||
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
@@ -443,7 +451,7 @@ async def setup_climate_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_climate(var, config):
|
||||
async def register_climate(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("climate", config)
|
||||
@@ -451,7 +459,7 @@ async def register_climate(var, config):
|
||||
await setup_climate_core_(var, config)
|
||||
|
||||
|
||||
async def new_climate(config, *args):
|
||||
async def new_climate(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_climate(var, config)
|
||||
return var
|
||||
@@ -485,7 +493,12 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
async def climate_control_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
@@ -549,5 +562,5 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(climate_ns.using)
|
||||
|
||||
@@ -511,29 +511,6 @@ ClimateTraits Climate::get_traits() {
|
||||
return traits;
|
||||
}
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
|
||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
ClimateCall Climate::make_call() { return ClimateCall(this); }
|
||||
|
||||
ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
|
||||
@@ -228,11 +228,22 @@ class Climate : public EntityBase {
|
||||
ClimateTraits get_traits();
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override);
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override);
|
||||
void set_visual_temperature_step_override(float target, float current);
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
void set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
|
||||
|
||||
@@ -46,7 +46,7 @@ from esphome.core.entity_helpers import (
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj, MockObjClass
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -162,7 +162,7 @@ _COVER_SCHEMA = (
|
||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||
|
||||
|
||||
def _validate_mqtt_state_topics(config):
|
||||
def _validate_mqtt_state_topics(config: ConfigType) -> ConfigType:
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
if CONF_POSITION_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
@@ -201,7 +201,7 @@ def cover_schema(
|
||||
|
||||
|
||||
@setup_entity("cover")
|
||||
async def setup_cover_core_(var, config):
|
||||
async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
|
||||
setup_device_class(config)
|
||||
|
||||
if CONF_ON_OPEN in config:
|
||||
@@ -235,7 +235,7 @@ async def setup_cover_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_cover(var, config):
|
||||
async def register_cover(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("cover", config)
|
||||
@@ -243,7 +243,7 @@ async def register_cover(var, config):
|
||||
await setup_cover_core_(var, config)
|
||||
|
||||
|
||||
async def new_cover(config, *args):
|
||||
async def new_cover(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_cover(var, config)
|
||||
return var
|
||||
@@ -259,7 +259,12 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
async def cover_open_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -267,7 +272,12 @@ async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
async def cover_close_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -275,7 +285,12 @@ async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
async def cover_stop_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -283,7 +298,12 @@ async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_toggle_to_code(config, action_id, template_arg, args):
|
||||
async def cover_toggle_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -421,5 +441,5 @@ automation.register_condition(
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(cover_ns.using)
|
||||
|
||||
@@ -135,10 +135,6 @@ CoverCall &CoverCall::set_stop(bool stop) {
|
||||
this->stop_ = stop;
|
||||
return *this;
|
||||
}
|
||||
bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
|
||||
@@ -184,9 +180,6 @@ optional<CoverRestoreState> Cover::restore_state_() {
|
||||
return recovered;
|
||||
}
|
||||
|
||||
bool Cover::is_fully_open() const { return this->position == COVER_OPEN; }
|
||||
bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; }
|
||||
|
||||
CoverCall CoverRestoreState::to_call(Cover *cover) {
|
||||
auto call = cover->make_call();
|
||||
auto traits = cover->get_traits();
|
||||
|
||||
@@ -50,7 +50,7 @@ class CoverCall {
|
||||
void perform();
|
||||
|
||||
const optional<float> &get_position() const;
|
||||
bool get_stop() const;
|
||||
bool get_stop() const { return this->stop_; }
|
||||
const optional<float> &get_tilt() const;
|
||||
const optional<bool> &get_toggle() const;
|
||||
|
||||
@@ -123,7 +123,7 @@ class Cover : public EntityBase {
|
||||
float tilt{COVER_OPEN};
|
||||
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
CoverCall make_call() { return {this}; }
|
||||
|
||||
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
|
||||
|
||||
@@ -139,9 +139,9 @@ class Cover : public EntityBase {
|
||||
virtual CoverTraits get_traits() = 0;
|
||||
|
||||
/// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0
|
||||
bool is_fully_open() const;
|
||||
bool is_fully_open() const { return this->position == COVER_OPEN; }
|
||||
/// Helper method to check if the cover is fully closed. Equivalent to comparing .position against 0.0
|
||||
bool is_fully_closed() const;
|
||||
bool is_fully_closed() const { return this->position == COVER_CLOSED; }
|
||||
|
||||
protected:
|
||||
friend CoverCall;
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ruamel.yaml import YAML
|
||||
@@ -13,6 +14,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
from esphome.happy_eyeballs import ensure_happy_eyeballs
|
||||
from esphome.types import ConfigType
|
||||
from esphome.yaml_util import dump
|
||||
|
||||
dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
|
||||
@@ -23,14 +25,14 @@ DEPENDENCIES = ["api"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def validate_import_url(value):
|
||||
def validate_import_url(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
value = cv.Length(max=255)(value)
|
||||
validate_source_shorthand(value)
|
||||
return value
|
||||
|
||||
|
||||
def validate_full_url(config):
|
||||
def validate_full_url(config: ConfigType) -> ConfigType:
|
||||
if not config[CONF_IMPORT_FULL_CONFIG]:
|
||||
return config
|
||||
source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL])
|
||||
@@ -55,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()[CONF_ESPHOME]
|
||||
if CONF_PROJECT not in full_config:
|
||||
raise cv.Invalid(
|
||||
@@ -73,7 +75,7 @@ wifi:
|
||||
"""
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_DASHBOARD_IMPORT")
|
||||
url = config[CONF_PACKAGE_IMPORT_URL]
|
||||
if config[CONF_IMPORT_FULL_CONFIG]:
|
||||
|
||||
@@ -37,8 +37,6 @@ void DateEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
void DateCall::validate_() {
|
||||
if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
|
||||
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
|
||||
|
||||
@@ -96,6 +96,8 @@ class DateCall {
|
||||
optional<uint8_t> day_;
|
||||
};
|
||||
|
||||
inline DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
template<typename... Ts> class DateSetAction final : public Action<Ts...>, public Parented<DateEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, date)
|
||||
|
||||
@@ -53,8 +53,6 @@ void DateTimeEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
ESPTime DateTimeEntity::state_as_esptime() const {
|
||||
ESPTime obj;
|
||||
obj.year = this->year_;
|
||||
|
||||
@@ -121,6 +121,8 @@ class DateTimeCall {
|
||||
optional<uint8_t> second_;
|
||||
};
|
||||
|
||||
inline DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
template<typename... Ts> class DateTimeSetAction final : public Action<Ts...>, public Parented<DateTimeEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, datetime)
|
||||
|
||||
@@ -33,8 +33,6 @@ void TimeEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
void TimeCall::validate_() {
|
||||
if (this->hour_.has_value() && this->hour_ > 23) {
|
||||
ESP_LOGE(TAG, "Hour must be between 0 and 23");
|
||||
|
||||
@@ -98,6 +98,8 @@ class TimeCall {
|
||||
optional<uint8_t> second_;
|
||||
};
|
||||
|
||||
inline TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
template<typename... Ts> class TimeSetAction final : public Action<Ts...>, public Parented<TimeEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, time)
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger"]
|
||||
@@ -45,7 +46,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("HWINFO", True)
|
||||
# gdb thread support
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
UNIT_MILLISECOND,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
@@ -111,7 +112,7 @@ CONFIG_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
if free_conf := config.get(CONF_FREE):
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
ICON_CHIP,
|
||||
ICON_RESTART,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
@@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
if CONF_DEVICE in config:
|
||||
|
||||
@@ -163,6 +163,11 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
"You need to remove the global wakeup_pin_mode and define it per pin"
|
||||
)
|
||||
if wakeup_pins:
|
||||
if CONF_WAKEUP_PIN_MODE in wakeup_pins[0]:
|
||||
raise cv.Invalid(
|
||||
"Specify wakeup_pin_mode either at the top level under deep_sleep "
|
||||
"or under the pin entry, not both"
|
||||
)
|
||||
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
|
||||
elif (
|
||||
isinstance(config.get(CONF_WAKEUP_PIN), list)
|
||||
|
||||
@@ -43,10 +43,6 @@ void DeepSleepComponent::loop() {
|
||||
this->begin_sleep();
|
||||
}
|
||||
|
||||
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
|
||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
|
||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->prevent_ && !manual) {
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
@@ -76,8 +72,4 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
|
||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
|
||||
|
||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
|
||||
|
||||
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
} // namespace esphome::deep_sleep
|
||||
|
||||
@@ -132,7 +132,7 @@ template<typename... Ts> class PreventDeepSleepAction;
|
||||
class DeepSleepComponent final : public Component {
|
||||
public:
|
||||
/// Set the duration in ms the component should sleep once it's in deep sleep mode.
|
||||
void set_sleep_duration(uint32_t time_ms);
|
||||
void set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
#if defined(USE_ESP32)
|
||||
/** Set the pin to wake up to on the ESP32 once it's in deep sleep mode.
|
||||
* Use the inverted property to set the wakeup level.
|
||||
@@ -157,7 +157,7 @@ class DeepSleepComponent final : public Component {
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
void set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
// Set the duration in ms for how long the code should run before entering
|
||||
@@ -166,7 +166,7 @@ class DeepSleepComponent final : public Component {
|
||||
#endif // USE_ESP32
|
||||
|
||||
/// Set a duration in ms for how long the code should run before entering deep sleep mode.
|
||||
void set_run_duration(uint32_t time_ms);
|
||||
void set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -176,8 +176,8 @@ class DeepSleepComponent final : public Component {
|
||||
/// Helper to enter deep sleep mode
|
||||
void begin_sleep(bool manual = false);
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
void prevent_deep_sleep() { this->prevent_ = true; }
|
||||
void allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
|
||||
@@ -74,12 +74,6 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
|
||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
|
||||
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
|
||||
}
|
||||
|
||||
@@ -685,9 +685,6 @@ void Display::show_page(DisplayPage *page) {
|
||||
}
|
||||
}
|
||||
|
||||
void Display::show_next_page() { this->page_->show_next(); }
|
||||
void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
|
||||
void Display::do_update_() {
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
@@ -892,9 +889,6 @@ void DisplayPage::show_prev() {
|
||||
this->prev_->show();
|
||||
}
|
||||
|
||||
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
|
||||
|
||||
const LogString *text_align_to_string(TextAlign textalign) {
|
||||
|
||||
@@ -802,9 +802,9 @@ class DisplayPage final {
|
||||
void show();
|
||||
void show_next();
|
||||
void show_prev();
|
||||
void set_parent(Display *parent);
|
||||
void set_prev(DisplayPage *prev);
|
||||
void set_next(DisplayPage *next);
|
||||
void set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &get_writer() const;
|
||||
|
||||
protected:
|
||||
@@ -814,6 +814,9 @@ class DisplayPage final {
|
||||
DisplayPage *next_{nullptr};
|
||||
};
|
||||
|
||||
inline void Display::show_next_page() { this->page_->show_next(); }
|
||||
inline void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
|
||||
template<typename... Ts> class DisplayPageShowAction final : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(DisplayPage *, page)
|
||||
|
||||
@@ -233,6 +233,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||
"esp_eth", # Ethernet driver - only needed by ethernet component
|
||||
"esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back
|
||||
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||
"esp_http_client", # HTTP client - only needed by http_request component
|
||||
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
|
||||
|
||||
@@ -124,6 +124,15 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou
|
||||
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
|
||||
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
|
||||
static constexpr uint32_t CRASH_DATA_VERSION = 4;
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's
|
||||
// cause/vaddr slots were never written (not a real exception frame).
|
||||
static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM;
|
||||
#elif CONFIG_IDF_TARGET_ARCH_RISCV
|
||||
// Synchronous mcause exception codes are small and have no interrupt bit;
|
||||
// anything else in a non-pseudo record is a stale slot.
|
||||
static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32;
|
||||
#endif
|
||||
struct RawCrashData {
|
||||
uint32_t version;
|
||||
uint32_t magic;
|
||||
@@ -198,10 +207,28 @@ void crash_handler_clear() {
|
||||
s_raw_crash_data.magic = 0;
|
||||
}
|
||||
|
||||
// Whether the cause slot was written by a real exception frame.
|
||||
static bool cause_slot_was_written() {
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT;
|
||||
#else
|
||||
return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Look up the exception cause as a human-readable string.
|
||||
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
|
||||
// not exposed via any public API.
|
||||
static const char *get_exception_reason() {
|
||||
uint8_t exception = s_raw_crash_data.exception;
|
||||
if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) {
|
||||
// Abort-class panics carry no cause register
|
||||
return nullptr;
|
||||
}
|
||||
if (!cause_slot_was_written()) {
|
||||
// Garbage from old-build or corrupt records; report just the type
|
||||
return nullptr;
|
||||
}
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
if (s_raw_crash_data.pseudo_excause) {
|
||||
// SoC-level panic: watchdog, cache error, etc.
|
||||
@@ -354,10 +381,11 @@ static const char *const FAULT_ADDR_REG = "MTVAL";
|
||||
static const char *const FAULT_ADDR_REG_LOWER = "mtval";
|
||||
#endif
|
||||
|
||||
// Whether the fault address is meaningful — real CPU faults only, not
|
||||
// aborts/watchdogs or SoC-level pseudo exceptions.
|
||||
// Whether the fault address is meaningful: real CPU faults with a validly
|
||||
// written frame only.
|
||||
static bool has_fault_addr() {
|
||||
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
|
||||
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause &&
|
||||
cause_slot_was_written();
|
||||
}
|
||||
|
||||
// The record was captured by a different firmware build (it survives soft
|
||||
@@ -458,6 +486,10 @@ void crash_handler_log() {
|
||||
// into NOINIT memory before the normal panic handler runs.
|
||||
//
|
||||
extern "C" {
|
||||
// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an
|
||||
// abort; weak so builds without the task watchdog still link.
|
||||
extern bool g_twdt_isr __attribute__((weak));
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
// Names are mandated by the --wrap linker mechanism
|
||||
extern void __real_esp_panic_handler(panic_info_t *info);
|
||||
@@ -470,6 +502,14 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
s_raw_crash_data.exception = (uint8_t) info->exception;
|
||||
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
|
||||
s_raw_crash_data.crashed_core = (uint8_t) info->core;
|
||||
if (g_panic_abort) {
|
||||
// IDF reclassifies to ABORT only inside esp_panic_handler(), after this
|
||||
// wrapper captured info->exception; correct it here. TWDT is our own
|
||||
// distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is
|
||||
// not stored; the symbolized backtrace already identifies the site.
|
||||
bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr;
|
||||
s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT);
|
||||
}
|
||||
// Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
|
||||
s_raw_crash_data.cause = 0;
|
||||
s_raw_crash_data.fault_addr = 0;
|
||||
@@ -487,8 +527,12 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
// Xtensa: walk the backtrace using the public API
|
||||
if (info->frame != nullptr) {
|
||||
auto *xt_frame = (XtExcFrame *) info->frame;
|
||||
s_raw_crash_data.cause = xt_frame->exccause;
|
||||
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
|
||||
if (!g_panic_abort) {
|
||||
// Abort-class frames carry no useful cause/vaddr: TWDT task snapshots
|
||||
// never wrote them and abort() traps describe only the synthetic trap.
|
||||
s_raw_crash_data.cause = xt_frame->exccause;
|
||||
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
|
||||
}
|
||||
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
|
||||
}
|
||||
|
||||
@@ -510,8 +554,11 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
// RISC-V: capture MEPC + RA, then scan stack for code addresses
|
||||
if (info->frame != nullptr) {
|
||||
auto *rv_frame = (RvExcFrame *) info->frame;
|
||||
s_raw_crash_data.cause = rv_frame->mcause;
|
||||
s_raw_crash_data.fault_addr = rv_frame->mtval;
|
||||
if (!g_panic_abort) {
|
||||
// See the Xtensa branch: abort-class frames carry no valid cause/vaddr.
|
||||
s_raw_crash_data.cause = rv_frame->mcause;
|
||||
s_raw_crash_data.fault_addr = rv_frame->mtval;
|
||||
}
|
||||
s_raw_crash_data.backtrace_count =
|
||||
capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
|
||||
}
|
||||
|
||||
@@ -643,8 +643,28 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
App.wake_loop_threadsafe();
|
||||
return;
|
||||
|
||||
// Log the result of connection parameter updates: a peer can reject or
|
||||
// never answer an update, and without this the link silently stays on the
|
||||
// old parameters (visible only as unexplained supervision timeouts).
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: {
|
||||
if (param->update_conn_params.status != ESP_BT_STATUS_SUCCESS) {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
|
||||
ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status);
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
else {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
|
||||
ESP_LOGV(TAG, "[%s] Conn params updated: interval=%u (x1.25ms) latency=%u timeout=%u (x10ms)", mac_s,
|
||||
param->update_conn_params.conn_int, param->update_conn_params.latency,
|
||||
param->update_conn_params.timeout);
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore these GAP events as they are not relevant for our use case
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
|
||||
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
|
||||
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
|
||||
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -31,6 +32,7 @@ from esphome.core import (
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import IS_MACOS, copy_file_if_changed
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||
@@ -88,7 +90,7 @@ def lambdas_use_scanf_float(config: ConfigType) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_ESP8266] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
|
||||
@@ -102,7 +104,7 @@ def set_core_data(config):
|
||||
return config
|
||||
|
||||
|
||||
def get_download_types(storage_json):
|
||||
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
|
||||
"""Binary-download entries for a built ESP8266 firmware.
|
||||
|
||||
Used by device-builder (esphome/device-builder), via
|
||||
@@ -157,7 +159,7 @@ ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
||||
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
||||
|
||||
|
||||
def _arduino_check_versions(value):
|
||||
def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"),
|
||||
@@ -200,7 +202,7 @@ def _arduino_check_versions(value):
|
||||
return value
|
||||
|
||||
|
||||
def _parse_platform_version(value):
|
||||
def _parse_platform_version(value: Any) -> str:
|
||||
try:
|
||||
# if platform version is a valid version constraint, prefix the default package
|
||||
cv.platformio_version_constraint(value)
|
||||
@@ -275,7 +277,7 @@ def check_rosetta() -> None:
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add(esp8266_ns.setup_preferences())
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
@@ -504,7 +506,7 @@ ESP8266_EXCEPTION_CODES = {
|
||||
}
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
def _decode_pc(config: ConfigType, addr: str) -> None:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
idedata = toolchain.get_idedata(config)
|
||||
@@ -525,7 +527,7 @@ def _decode_pc(config, addr):
|
||||
_LOGGER.warning("Decoded %s", translation)
|
||||
|
||||
|
||||
def _parse_register(config, regex, line):
|
||||
def _parse_register(config: ConfigType, regex: re.Pattern[str], line: str) -> None:
|
||||
match = regex.match(line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
@@ -549,7 +551,7 @@ STACKTRACE_BAD_ALLOC_RE = re.compile(
|
||||
STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
|
||||
|
||||
def process_stacktrace(config, line, backtrace_state):
|
||||
def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool:
|
||||
line = line.strip()
|
||||
# ESP8266 Exception type
|
||||
match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line)
|
||||
|
||||
@@ -118,8 +118,6 @@ static const LogString *get_exception_cause(uint32_t cause) {
|
||||
}
|
||||
|
||||
static const LogString *get_reset_reason(uint32_t reason) {
|
||||
if (reason == REASON_WDT_RST)
|
||||
return LOG_STR("Hardware WDT");
|
||||
if (reason == REASON_EXCEPTION_RST)
|
||||
return LOG_STR("Exception");
|
||||
if (reason == REASON_SOFT_WDT_RST)
|
||||
@@ -162,13 +160,20 @@ void crash_handler_log() {
|
||||
if (!is_crash_reason(resetInfo.reason))
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
if (resetInfo.reason == REASON_WDT_RST) {
|
||||
// A hardware WDT reset happens entirely in hardware: the postmortem hook
|
||||
// never runs, so rst_info epc1/exccause and the RTC backtrace are
|
||||
// leftovers from an earlier crash. Don't misattribute them (#18596).
|
||||
ESP_LOGE(TAG, " Reason: Hardware WDT (no crash state is recorded for hardware WDT resets)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost).
|
||||
// Both resetInfo and RTC data survive until the next reset, so this can be
|
||||
// called multiple times (logger init + API subscribe) with the same result.
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
|
||||
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
|
||||
// the Arduino core's postmortem handler behavior.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -18,6 +19,8 @@ from esphome.const import (
|
||||
PLATFORM_ESP8266,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import boards
|
||||
from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
|
||||
@@ -27,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _lookup_pin(value):
|
||||
def _lookup_pin(value: str) -> int:
|
||||
board = CORE.data[KEY_ESP8266][KEY_BOARD]
|
||||
board_pins = boards.ESP8266_BOARD_PINS.get(board, {})
|
||||
|
||||
@@ -42,7 +45,7 @@ def _lookup_pin(value):
|
||||
raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.")
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
def _translate_pin(value: Any) -> int:
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -69,7 +72,7 @@ _ESP_SDIO_PINS = {
|
||||
}
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
def validate_gpio_pin(value: Any) -> int:
|
||||
value = _translate_pin(value)
|
||||
if value < 0 or value > 17:
|
||||
raise cv.Invalid(f"ESP8266: Invalid pin number: {value}")
|
||||
@@ -86,7 +89,7 @@ def validate_gpio_pin(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_supports(value):
|
||||
def validate_supports(value: ConfigType) -> ConfigType:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
@@ -160,7 +163,7 @@ class PinInitialState:
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA)
|
||||
async def esp8266_pin_to_code(config):
|
||||
async def esp8266_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
mode = config[CONF_MODE]
|
||||
@@ -192,7 +195,7 @@ async def esp8266_pin_to_code(config):
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
async def add_pin_initial_states_array():
|
||||
async def add_pin_initial_states_array() -> None:
|
||||
# Add includes at the very end, so that they override everything
|
||||
initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][
|
||||
KEY_PIN_INITIAL_STATES
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.noise import decode_encryption_key, encryption_schema
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||
from esphome.config_helpers import merge_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_API,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_ESPHOME,
|
||||
CONF_ID,
|
||||
CONF_KEY,
|
||||
CONF_NUM_ATTEMPTS,
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
@@ -15,6 +19,7 @@ from esphome.const import (
|
||||
CONF_REBOOT_TIMEOUT,
|
||||
CONF_SAFE_MODE,
|
||||
CONF_VERSION,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
@@ -30,7 +35,15 @@ CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
|
||||
AUTO_LOAD = ["sha256", "socket"]
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
"""Auto-load noise only when encryption is configured."""
|
||||
base = ["sha256", "socket"]
|
||||
# A falsy config is a tooling probe for the maximal set (None from
|
||||
# dependency resolution, {} from the components-graph platform probe);
|
||||
# a validated config always carries defaults, never empty
|
||||
if not config or CONF_ENCRYPTION in config:
|
||||
return base + ["noise"]
|
||||
return base
|
||||
|
||||
|
||||
esphome = cg.esphome_ns.namespace("esphome")
|
||||
@@ -67,11 +80,24 @@ def ota_esphome_final_validate(config):
|
||||
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
|
||||
and CONF_PASSWORD in ota_conf
|
||||
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
|
||||
!= ota_conf.get(CONF_PASSWORD)
|
||||
!= ota_conf[CONF_PASSWORD]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
|
||||
)
|
||||
# Encryption blocks conflict only when both pin a key; a bare
|
||||
# `encryption:` (a package/device split) is compatible with a
|
||||
# keyed one, and merge_config yields the keyed result
|
||||
merged_key = (
|
||||
merged_ota_esphome_configs_by_port[conf_port]
|
||||
.get(CONF_ENCRYPTION, {})
|
||||
.get(CONF_KEY)
|
||||
)
|
||||
other_key = ota_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
|
||||
if merged_key and other_key and merged_key != other_key:
|
||||
raise cv.Invalid(
|
||||
f"Found multiple configurations but {CONF_ENCRYPTION} is inconsistent"
|
||||
)
|
||||
|
||||
ports_with_merged_configs.append(conf_port)
|
||||
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
|
||||
@@ -94,6 +120,73 @@ def ota_esphome_final_validate(config):
|
||||
|
||||
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
|
||||
|
||||
# There is one encryption key per device: when the api component has one,
|
||||
# ota uses it, and an explicit ota key must match it. A bare `encryption:`
|
||||
# block resolves to the api key here so both codegen and the upload CLI
|
||||
# see the actual key.
|
||||
api_conf = full_conf.get(CONF_API) or {}
|
||||
api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
|
||||
has_web_server_ota = any(
|
||||
conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf
|
||||
)
|
||||
for ota_conf in merged_ota_esphome_configs_by_port.values():
|
||||
# Merging same-port blocks can combine a password from one block with
|
||||
# encryption from another; re-check the exclusion on the merged result.
|
||||
_validate_no_password_with_encryption(ota_conf)
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is None:
|
||||
continue
|
||||
if has_web_server_ota:
|
||||
# The web_server ota platform accepts the same image over plain
|
||||
# HTTP with basic auth, a full bypass of the encryption.
|
||||
if CONF_WEB_SERVER in full_conf:
|
||||
# With the web_server component the endpoint is always on;
|
||||
# fail closed like the password combination
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} cannot be combined with the "
|
||||
f"'{CONF_WEB_SERVER}' component; its '{CONF_OTA}' platform "
|
||||
f"accepts the same image over plaintext HTTP, remove one of them"
|
||||
)
|
||||
# Without the component the platform is the captive_portal
|
||||
# auto-load: the endpoint only exists while the fallback AP is
|
||||
# active, so keep the recovery path and warn instead
|
||||
_LOGGER.warning(
|
||||
"OTA encryption does not cover the %s OTA platform (auto-loaded "
|
||||
"by captive_portal); the plaintext /update endpoint stays "
|
||||
"reachable while the fallback AP is active",
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
if ota_key := encryption_conf.get(CONF_KEY):
|
||||
if api_key and ota_key != api_key:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY} must match the "
|
||||
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY}; omit the "
|
||||
f"'{CONF_OTA}' {CONF_KEY} to use the '{CONF_API}' one"
|
||||
)
|
||||
elif not api_key:
|
||||
if CONF_ENCRYPTION in api_conf:
|
||||
# A keyless `api: encryption:` block gets its key provisioned
|
||||
# at runtime and stored in flash, so there is nothing to
|
||||
# inherit at build time
|
||||
raise cv.Invalid(
|
||||
f"the '{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} is provisioned at "
|
||||
f"runtime and cannot be inherited at build time; set an explicit "
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY}"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} has no {CONF_KEY} and there is no "
|
||||
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} to inherit; set one of them"
|
||||
)
|
||||
else:
|
||||
encryption_conf[CONF_KEY] = api_key
|
||||
# The device treats the all-zeros PSK as "no key configured" (it is the
|
||||
# api provisioning sentinel), so letting it through would leave the OTA
|
||||
# port accepting plaintext while the YAML says encryption. Fail closed.
|
||||
if not any(decode_encryption_key(encryption_conf[CONF_KEY])):
|
||||
raise cv.Invalid(
|
||||
f"The all-zeros {CONF_KEY} is reserved and provides no protection; "
|
||||
f"generate a real key with: openssl rand -base64 32"
|
||||
)
|
||||
|
||||
full_conf[CONF_OTA] = new_ota_conf
|
||||
fv.full_config.set(full_conf)
|
||||
|
||||
@@ -107,6 +200,17 @@ def ota_esphome_final_validate(config):
|
||||
)
|
||||
|
||||
|
||||
# Not cv.has_at_most_one_key: this message explains the why, and the check is
|
||||
# reused on merged same-port configs in final validate where schemas do not run
|
||||
def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
|
||||
if CONF_PASSWORD in config and CONF_ENCRYPTION in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the "
|
||||
f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for OTA component."""
|
||||
from esphome.components import socket
|
||||
@@ -134,6 +238,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.port,
|
||||
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PASSWORD): cv.sensitive(),
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
|
||||
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
|
||||
),
|
||||
@@ -147,12 +252,24 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(BASE_OTA_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_no_password_with_encryption,
|
||||
_consume_ota_sockets,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Filter out the noise transport when no ota entry configures encryption."""
|
||||
for ota_conf in CORE.config.get(CONF_OTA, []):
|
||||
if (
|
||||
ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
and ota_conf.get(CONF_ENCRYPTION) is not None
|
||||
):
|
||||
return []
|
||||
return ["ota_esphome_noise.cpp"]
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -171,6 +288,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config.get(CONF_ALLOW_PARTITION_ACCESS):
|
||||
cg.add_define("USE_OTA_PARTITIONS")
|
||||
|
||||
if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None:
|
||||
# A missing key was resolved from the api component in final validate.
|
||||
key = encryption_conf[CONF_KEY]
|
||||
cg.add_define("USE_OTA_ENCRYPTION")
|
||||
cg.add(var.set_noise_psk(list(decode_encryption_key(key))))
|
||||
|
||||
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
|
||||
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace esphome {
|
||||
|
||||
static const char *const TAG = "esphome.ota";
|
||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
|
||||
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
|
||||
|
||||
@@ -105,6 +104,11 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Password configured");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Encryption configured");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Partition access allowed\n"
|
||||
@@ -148,8 +152,10 @@ void ESPHomeOTAComponent::loop() {
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
|
||||
|
||||
void ESPHomeOTAComponent::handle_handshake_() {
|
||||
/// Handle the OTA handshake and authentication.
|
||||
@@ -201,8 +207,7 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
}
|
||||
|
||||
// Validate magic bytes
|
||||
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
|
||||
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
|
||||
if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) {
|
||||
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
|
||||
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
|
||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
|
||||
@@ -234,6 +239,19 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
}
|
||||
this->ota_features_ = this->handshake_buf_[0];
|
||||
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// Fail closed: with a PSK configured the client must negotiate encryption
|
||||
// (which requires the extended protocol); refuse plaintext uploads.
|
||||
static constexpr uint8_t NOISE_REQUIRED_FEATURES =
|
||||
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
|
||||
if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) {
|
||||
ESP_LOGW(TAG, "Client does not support encryption");
|
||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->transition_ota_state_(OTAState::FEATURE_ACK);
|
||||
|
||||
const bool supports_compression =
|
||||
@@ -249,6 +267,12 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
|
||||
#endif
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ctx_.has_psk()) {
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
|
||||
}
|
||||
this->server_feature_flags_ = this->handshake_buf_[1];
|
||||
#endif
|
||||
} else {
|
||||
this->handshake_buf_[0] =
|
||||
@@ -264,6 +288,18 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
|
||||
return;
|
||||
}
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// With a PSK configured the rest of the session runs inside the noise
|
||||
// transport; the client sends the first handshake frame next, so there
|
||||
// is nothing to do until data arrives.
|
||||
if (this->noise_ctx_.has_psk()) {
|
||||
if (!this->noise_start_session_()) {
|
||||
return;
|
||||
}
|
||||
this->transition_ota_state_(OTAState::NOISE_HANDSHAKE);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
// If password is set, move to auth phase
|
||||
if (!this->password_.empty()) {
|
||||
@@ -301,6 +337,16 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
this->handle_data_();
|
||||
return;
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
case OTAState::NOISE_HANDSHAKE:
|
||||
if (!this->handle_noise_handshake_()) {
|
||||
return;
|
||||
}
|
||||
this->transition_ota_state_(OTAState::DATA);
|
||||
this->handle_data_();
|
||||
return;
|
||||
#endif
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -360,12 +406,13 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
this->client_->setblocking(true);
|
||||
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
if (this->extended_proto_) {
|
||||
// Read ota type, 1 byte
|
||||
if (!this->readall_(buf, 1)) {
|
||||
if (!this->data_readall_(buf, 1)) {
|
||||
this->log_read_error_(LOG_STR("OTA type"));
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ota_type = static_cast<ota::OTAType>(buf[0]);
|
||||
@@ -373,8 +420,9 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
|
||||
|
||||
// Read size, 4 bytes MSB first
|
||||
if (!this->readall_(buf, 4)) {
|
||||
if (!this->data_readall_(buf, 4)) {
|
||||
this->log_read_error_(LOG_STR("size"));
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
|
||||
@@ -404,11 +452,12 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
|
||||
// Acknowledge prepare OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||
|
||||
// Read binary MD5, 32 bytes
|
||||
if (!this->readall_(buf, 32)) {
|
||||
if (!this->data_readall_(buf, 32)) {
|
||||
this->log_read_error_(LOG_STR("MD5 checksum"));
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
sbuf[32] = '\0';
|
||||
@@ -416,7 +465,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
this->backend_->set_update_md5(sbuf);
|
||||
|
||||
// Acknowledge MD5 OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
|
||||
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
||||
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
||||
@@ -432,19 +481,37 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
size_t remaining = ota_size - total;
|
||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||
ssize_t read = this->client_->read(buf, requested);
|
||||
if (read == -1) {
|
||||
const int err = errno;
|
||||
if (this->would_block_(err)) {
|
||||
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
|
||||
App.feed_wdt();
|
||||
continue;
|
||||
ssize_t read;
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr) {
|
||||
// One frame per call; noise_read_data_ waits internally (readall_), so
|
||||
// there is no would-block retry here and failures are already logged.
|
||||
read = this->noise_read_data_(buf, requested);
|
||||
if (read <= 0) {
|
||||
// error_code still holds the last OK; report a real failure instead
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
read = this->client_->read(buf, requested);
|
||||
if (read == -1) {
|
||||
const int err = errno;
|
||||
if (this->would_block_(err)) {
|
||||
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
|
||||
App.feed_wdt();
|
||||
continue;
|
||||
}
|
||||
ESP_LOGW(TAG, "Read err %d", err);
|
||||
// error_code still holds the last OK; report a real failure instead
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
} else if (read == 0) {
|
||||
ESP_LOGW(TAG, "Remote closed");
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ESP_LOGW(TAG, "Read err %d", err);
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
} else if (read == 0) {
|
||||
ESP_LOGW(TAG, "Remote closed");
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
last_data_ms = millis();
|
||||
@@ -456,7 +523,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
total += read;
|
||||
#if USE_OTA_VERSION == 2
|
||||
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
||||
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||
size_acknowledged += OTA_BLOCK_SIZE;
|
||||
}
|
||||
#endif
|
||||
@@ -475,7 +542,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
|
||||
// Acknowledge receive OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||
|
||||
error_code = this->backend_->end();
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
@@ -484,10 +551,10 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
|
||||
// Acknowledge Update end OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||
|
||||
// Read ACK
|
||||
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
this->log_read_error_(LOG_STR("ack"));
|
||||
// do not go to error, this is not fatal
|
||||
}
|
||||
@@ -510,7 +577,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
App.safe_reboot();
|
||||
|
||||
error:
|
||||
this->write_byte_(static_cast<uint8_t>(error_code));
|
||||
this->data_write_byte_(static_cast<uint8_t>(error_code));
|
||||
|
||||
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
|
||||
// Always call abort() unconditionally: backends register external partitions before
|
||||
@@ -588,8 +655,6 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
|
||||
}
|
||||
|
||||
float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
|
||||
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
@@ -679,6 +744,9 @@ void ESPHomeOTAComponent::cleanup_connection_() {
|
||||
this->backend_ = nullptr;
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
this->cleanup_auth_();
|
||||
#endif
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
this->noise_ = nullptr;
|
||||
#endif
|
||||
// Intentionally no disable_loop() — letting loop() run one more iteration catches
|
||||
// any connection that queued on the listener mid-session (otherwise the wake flag,
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#ifdef USE_OTA
|
||||
#include "esphome/components/ota/ota_backend_factory.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
#include "esphome/components/noise/noise_handshake.h"
|
||||
#endif
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
@@ -24,7 +27,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
AUTH_SEND, // Sending authentication request
|
||||
AUTH_READ, // Reading authentication data
|
||||
#endif // USE_OTA_PASSWORD
|
||||
DATA, // BLOCKING! Processing OTA data (update, etc.)
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
NOISE_HANDSHAKE, // Exchanging Noise handshake frames
|
||||
#endif
|
||||
DATA, // BLOCKING! Processing OTA data (update, etc.)
|
||||
};
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
void set_auth_password(const std::string &password) { password_ = password; }
|
||||
@@ -38,15 +44,19 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
}
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
#endif
|
||||
|
||||
/// Manually set the port OTA should listen on
|
||||
void set_port(uint16_t port);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
uint16_t get_port() const;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
|
||||
protected:
|
||||
void handle_handshake_();
|
||||
@@ -63,6 +73,48 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
bool writeall_(const uint8_t *buf, size_t len);
|
||||
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// Heap-allocated only while an encrypted OTA session is active.
|
||||
struct NoiseSession {
|
||||
~NoiseSession();
|
||||
noise::NoiseResponderHandshake handshake;
|
||||
NoiseCipherState *send_cipher{nullptr};
|
||||
NoiseCipherState *recv_cipher{nullptr};
|
||||
uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then
|
||||
uint16_t frame_pos{0}; // bytes read or written so far
|
||||
bool writing{false}; // a produced handshake frame is still being flushed
|
||||
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
|
||||
};
|
||||
bool noise_start_session_();
|
||||
bool handle_noise_handshake_();
|
||||
bool noise_try_read_frame_();
|
||||
bool noise_try_write_frame_();
|
||||
void noise_send_reject_(const LogString *reason);
|
||||
ssize_t noise_decrypt_(uint8_t *buf, size_t len);
|
||||
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
|
||||
bool noise_readall_(uint8_t *buf, size_t len);
|
||||
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
|
||||
bool noise_write_byte_(uint8_t byte);
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
|
||||
// Data-phase I/O dispatch: through the noise transport when a session is
|
||||
// active, straight to the socket otherwise.
|
||||
inline bool data_write_byte_(uint8_t byte) {
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr)
|
||||
return this->noise_write_byte_(byte);
|
||||
#endif
|
||||
return this->write_byte_(byte);
|
||||
}
|
||||
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
|
||||
inline bool data_readall_(uint8_t *buf, size_t len) {
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr)
|
||||
return this->noise_readall_(buf, len);
|
||||
#endif
|
||||
return this->readall_(buf, len);
|
||||
}
|
||||
|
||||
bool try_read_(size_t to_read, const LogString *desc);
|
||||
bool try_write_(size_t to_write, const LogString *desc);
|
||||
|
||||
@@ -91,6 +143,11 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
std::string password_;
|
||||
std::unique_ptr<uint8_t[]> auth_buf_;
|
||||
#endif // USE_OTA_PASSWORD
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
noise::NoiseContext noise_ctx_;
|
||||
std::unique_ptr<NoiseSession> noise_;
|
||||
uint8_t server_feature_flags_{0}; // as sent in the feature ack, bound into the prologue
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
|
||||
socket::ListenSocket *server_{nullptr};
|
||||
std::unique_ptr<socket::Socket> client_;
|
||||
@@ -98,6 +155,15 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
|
||||
uint32_t client_connect_time_{0};
|
||||
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
|
||||
// Buffer size for OTA data transfer. The upload client derives its maximum
|
||||
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
|
||||
// minus the 16-byte MAC); both must change together.
|
||||
static constexpr size_t OTA_BUFFER_SIZE = 1024;
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// Shrinking the buffer would reject every frame a current CLI sends
|
||||
static_assert(OTA_BUFFER_SIZE >= 1008 + noise::MAC_SIZE, "OTA_BUFFER_SIZE must fit a full encrypted data frame");
|
||||
#endif
|
||||
static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
uint32_t running_app_offset_{0};
|
||||
size_t running_app_size_{0};
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
#include "ota_esphome.h"
|
||||
#ifdef USE_OTA
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
static const char *const TAG = "esphome.ota";
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
|
||||
#else
|
||||
static const char *const OTA_NOISE_PROLOGUE_INIT = "NoiseOTAInit";
|
||||
#endif
|
||||
static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = 12; // strlen("NoiseOTAInit")
|
||||
|
||||
ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
|
||||
if (this->send_cipher != nullptr) {
|
||||
noise_cipherstate_free(this->send_cipher);
|
||||
}
|
||||
if (this->recv_cipher != nullptr) {
|
||||
noise_cipherstate_free(this->recv_cipher);
|
||||
}
|
||||
}
|
||||
|
||||
/** Allocate the session and start the responder handshake.
|
||||
*
|
||||
* The prologue binds the whole plaintext preamble, so any tampering with the
|
||||
* negotiation (a stripped feature flag, a changed version) breaks the first
|
||||
* handshake MAC on either side:
|
||||
* "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags
|
||||
*/
|
||||
bool ESPHomeOTAComponent::noise_start_session_() {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession());
|
||||
if (this->noise_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Session allocation failed");
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + 5 + 2 + 1 + 2];
|
||||
#ifdef USE_ESP8266
|
||||
memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
|
||||
#else
|
||||
std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
|
||||
#endif
|
||||
uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
|
||||
// Magic bytes, already validated in MAGIC_READ
|
||||
std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
|
||||
p += sizeof(MAGIC_BYTES);
|
||||
// Our magic ack
|
||||
*p++ = ota::OTA_RESPONSE_OK;
|
||||
*p++ = USE_OTA_VERSION;
|
||||
// The feature byte the client sent
|
||||
*p++ = this->ota_features_;
|
||||
// The feature ack we sent (noise requires the extended protocol)
|
||||
*p++ = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
||||
*p++ = this->server_feature_flags_;
|
||||
|
||||
int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue));
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Drive the non-blocking handshake from loop(); returns true once the
|
||||
* transport ciphers are ready and the session can enter the data phase.
|
||||
* On failure the connection is cleaned up and false is returned.
|
||||
*/
|
||||
bool ESPHomeOTAComponent::handle_noise_handshake_() {
|
||||
NoiseSession &s = *this->noise_;
|
||||
while (true) {
|
||||
if (s.writing) {
|
||||
if (!this->noise_try_write_frame_()) {
|
||||
return false; // would block, or errored and cleaned up
|
||||
}
|
||||
s.writing = false;
|
||||
s.frame_pos = 0;
|
||||
s.frame_len = 0;
|
||||
}
|
||||
switch (s.handshake.action()) {
|
||||
case noise::NoiseResponderHandshake::Action::ACTION_READ: {
|
||||
if (!this->noise_try_read_frame_()) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
|
||||
s.frame_pos = 0;
|
||||
s.frame_len = 0;
|
||||
if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
|
||||
ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->noise_send_reject_(noise::reject_reason_for(err));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case noise::NoiseResponderHandshake::Action::ACTION_WRITE: {
|
||||
size_t msg_len = 0;
|
||||
int err =
|
||||
s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
const uint16_t payload_len = msg_len + 1;
|
||||
noise::write_frame_header(s.frame_buf, payload_len);
|
||||
s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
|
||||
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
|
||||
s.frame_pos = 0;
|
||||
s.writing = true;
|
||||
break;
|
||||
}
|
||||
case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: {
|
||||
int err = s.handshake.split(s.send_cipher, s.recv_cipher);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Noise handshake complete");
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
ESP_LOGW(TAG, "Bad handshake state");
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-blocking read of one handshake frame into the session buffer.
|
||||
bool ESPHomeOTAComponent::noise_try_read_frame_() {
|
||||
NoiseSession &s = *this->noise_;
|
||||
while (s.frame_pos < noise::FRAME_HEADER_SIZE) {
|
||||
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos);
|
||||
if (!this->handle_read_error_(read, LOG_STR("read noise header"))) {
|
||||
return false;
|
||||
}
|
||||
s.frame_pos += read;
|
||||
}
|
||||
if (s.frame_len == 0) {
|
||||
const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]);
|
||||
if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) {
|
||||
ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len);
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
|
||||
}
|
||||
while (s.frame_pos < s.frame_len) {
|
||||
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
|
||||
if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) {
|
||||
return false;
|
||||
}
|
||||
s.frame_pos += read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Non-blocking write of the pending session-buffer frame.
|
||||
bool ESPHomeOTAComponent::noise_try_write_frame_() {
|
||||
NoiseSession &s = *this->noise_;
|
||||
while (s.frame_pos < s.frame_len) {
|
||||
ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
|
||||
if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
|
||||
return false;
|
||||
}
|
||||
s.frame_pos += written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Best-effort explicit reject frame so the client can log a readable reason.
|
||||
void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
|
||||
uint8_t data[noise::FRAME_HEADER_SIZE + 1 + 32];
|
||||
static_assert(sizeof(data) - noise::FRAME_HEADER_SIZE >= noise::MAC_FAILURE_PAYLOAD_SIZE,
|
||||
"reject buffer must fit the MAC failure wire contract");
|
||||
const size_t payload_len =
|
||||
noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
|
||||
noise::write_frame_header(data, payload_len);
|
||||
this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
|
||||
}
|
||||
|
||||
/// Decrypt a ciphertext in place; returns the plaintext size or -1.
|
||||
ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) {
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf, len, len);
|
||||
int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
return -1;
|
||||
}
|
||||
return mbuf.size;
|
||||
}
|
||||
|
||||
/** Blocking read of one frame whose ciphertext size must be within the given
|
||||
* bounds, decrypted in place; returns the plaintext size, or -1 on error.
|
||||
* buf needs max_ciphertext capacity.
|
||||
*/
|
||||
ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
|
||||
uint8_t header[noise::FRAME_HEADER_SIZE];
|
||||
if (!this->readall_(header, sizeof(header))) {
|
||||
return -1;
|
||||
}
|
||||
const size_t ciphertext_len = encode_uint16(header[1], header[2]);
|
||||
if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) {
|
||||
ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len);
|
||||
return -1;
|
||||
}
|
||||
if (!this->readall_(buf, ciphertext_len)) {
|
||||
return -1;
|
||||
}
|
||||
return this->noise_decrypt_(buf, ciphertext_len);
|
||||
}
|
||||
|
||||
/** Blocking read of one frame whose plaintext must be exactly len bytes
|
||||
* (control units are one unit per frame). buf needs len + noise::MAC_SIZE
|
||||
* capacity; the plaintext lands at buf[0..len).
|
||||
*/
|
||||
bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
|
||||
return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
|
||||
}
|
||||
|
||||
/** Blocking read of one data-phase frame, decrypted in place; returns the
|
||||
* plaintext size, or -1 on error. buf is the OTA_BUFFER_SIZE data buffer.
|
||||
* The ciphertext must fit that buffer and its plaintext must fit what the
|
||||
* caller accepts (the remaining image bytes).
|
||||
*/
|
||||
ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
|
||||
const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
|
||||
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
|
||||
}
|
||||
|
||||
/// Blocking write of one response byte as an encrypted frame.
|
||||
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
|
||||
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
|
||||
frame[noise::FRAME_HEADER_SIZE] = byte;
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
|
||||
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
return false;
|
||||
}
|
||||
noise::write_frame_header(frame, mbuf.size);
|
||||
return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
#endif // USE_OTA
|
||||
@@ -10,14 +10,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-
|
||||
|
||||
EthernetComponent::EthernetComponent() { global_eth_component = this; }
|
||||
|
||||
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void EthernetComponent::notify_ip_state_listeners_() {
|
||||
auto ips = this->get_ip_addresses();
|
||||
|
||||
@@ -125,7 +125,7 @@ class EthernetComponent final : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::ETHERNET; }
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||
|
||||
@@ -146,9 +146,9 @@ class EthernetComponent final : public Component {
|
||||
esp_netif_t *get_esp_netif() { return this->eth_netif_; }
|
||||
#endif
|
||||
|
||||
void set_type(EthernetType type);
|
||||
void set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, MAC_ADDRESS_SIZE> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
@@ -159,9 +159,6 @@ class EthernetComponent final : public Component {
|
||||
const char *get_use_address() const { return this->use_address_; }
|
||||
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string get_eth_mac_address_pretty();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
eth_duplex_t get_duplex_mode();
|
||||
eth_speed_t get_link_speed();
|
||||
@@ -171,35 +168,35 @@ class EthernetComponent final : public Component {
|
||||
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(uint8_t interrupt_pin);
|
||||
void set_reset_pin(uint8_t reset_pin);
|
||||
void set_clock_speed(int clock_speed);
|
||||
void set_interface(spi_host_device_t interface);
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void set_polling_interval(uint32_t polling_interval);
|
||||
void set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void set_phy_addr(uint8_t phy_addr);
|
||||
void set_power_pin(int power_pin);
|
||||
void set_mdc_pin(uint8_t mdc_pin);
|
||||
void set_mdio_pin(uint8_t mdio_pin);
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode);
|
||||
void set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif // USE_ETHERNET_SPI
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(int8_t interrupt_pin);
|
||||
void set_reset_pin(int8_t reset_pin);
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
#endif // USE_RP2
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
|
||||
@@ -908,25 +908,7 @@ void EthernetComponent::dump_connect_params_() {
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
|
||||
@@ -946,11 +928,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE];
|
||||
|
||||
@@ -249,11 +249,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE];
|
||||
@@ -355,13 +350,6 @@ void EthernetComponent::dump_connect_params_() {
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
|
||||
}
|
||||
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
|
||||
// there is no clean enable/disable hook today. The YAML option is accepted on
|
||||
|
||||
@@ -153,11 +153,6 @@ void FanRestoreState::apply(Fan &fan) {
|
||||
fan.publish_state();
|
||||
}
|
||||
|
||||
FanCall Fan::turn_on() { return this->make_call().set_state(true); }
|
||||
FanCall Fan::turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall Fan::make_call() { return FanCall(*this); }
|
||||
|
||||
const char *Fan::find_preset_mode_(const char *preset_mode) {
|
||||
return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ class Fan : public EntityBase {
|
||||
/// The current direction of the fan
|
||||
FanDirection direction{FanDirection::FORWARD};
|
||||
|
||||
FanCall turn_on();
|
||||
FanCall turn_off();
|
||||
FanCall toggle();
|
||||
FanCall make_call();
|
||||
FanCall turn_on() { return this->make_call().set_state(true); }
|
||||
FanCall turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall make_call() { return FanCall(*this); }
|
||||
|
||||
/// Register a callback that will be called each time the state changes.
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
@@ -75,12 +76,12 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
|
||||
return external_files.compute_local_file_path(DOMAIN, url)
|
||||
|
||||
|
||||
def local_path(value):
|
||||
def local_path(value: str | ConfigType) -> str:
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
|
||||
def download_file(url, path):
|
||||
def download_file(url: str, path: Path) -> str:
|
||||
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
|
||||
# silently ignored on a per-run memo hit anyway (memos key by path).
|
||||
external_files.download_content(url, path)
|
||||
@@ -98,7 +99,7 @@ def download_gh_svg(value: str | ConfigType, source: str) -> str:
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value):
|
||||
def download_image(value: str | ConfigType) -> str:
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
@@ -146,7 +147,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
|
||||
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
def validate_file_shorthand(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
if (remote := _parse_remote_shorthand(value)) is not None:
|
||||
return download_file(remote.url, remote.path)
|
||||
@@ -163,8 +164,8 @@ LOCAL_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def mdi_schema(source):
|
||||
def validate_mdi(value):
|
||||
def mdi_schema(source: str) -> cv.All:
|
||||
def validate_mdi(value: ConfigType) -> str:
|
||||
return download_gh_svg(value, source)
|
||||
|
||||
return cv.All(
|
||||
@@ -259,7 +260,9 @@ async def new_image(config: ConfigType) -> MockObj:
|
||||
return var
|
||||
|
||||
|
||||
async def write_image(config, all_frames=False):
|
||||
async def write_image(
|
||||
config: ConfigType, all_frames: bool = False
|
||||
) -> tuple[MockObj, int, int, MockObj, MockObj, int]:
|
||||
path = Path(config[CONF_FILE])
|
||||
if not path.is_file():
|
||||
raise core.EsphomeError(f"Could not load image file {path}")
|
||||
|
||||
@@ -8,7 +8,8 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.core import ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -62,7 +63,7 @@ CONFIG_SCHEMA = _globals_schema
|
||||
|
||||
# Run with low priority so that namespaces are registered first
|
||||
@coroutine_with_priority(CoroPriority.LATE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
type_ = cg.RawExpression(config[CONF_TYPE])
|
||||
restore = config[CONF_RESTORE_VALUE]
|
||||
|
||||
@@ -104,7 +105,12 @@ async def to_code(config):
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
async def globals_set_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -68,7 +69,7 @@ def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
|
||||
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
|
||||
|
||||
|
||||
def _final_validate(config) -> None:
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if not use_interrupt:
|
||||
return
|
||||
@@ -124,7 +125,7 @@ def _final_validate(config) -> None:
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components.one_wire import OneWireBus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INTERLOCK, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -24,7 +25,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@OttoWinter", "@esphome/core"]
|
||||
homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
|
||||
|
||||
|
||||
def validate_entity_domain(platform, supported_domains):
|
||||
def validator(config):
|
||||
def validate_entity_domain(
|
||||
platform: str, supported_domains: Iterable[str]
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
domain = config[CONF_ENTITY_ID].split(".", 1)[0]
|
||||
if domain not in supported_domains:
|
||||
raise cv.Invalid(
|
||||
@@ -34,7 +40,7 @@ HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup_home_assistant_entity(var, config):
|
||||
def setup_home_assistant_entity(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).ex
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
|
||||
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await number.new_number(
|
||||
config,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).e
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
|
||||
@@ -36,7 +37,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TIMEZONE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import homeassistant_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await time_.register_time(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import KEY_HOST
|
||||
|
||||
@@ -22,7 +23,7 @@ AUTO_LOAD = ["network", "preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_HOST] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host"
|
||||
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_build_flag("-DUSE_HOST")
|
||||
cg.add_define("USE_NATIVE_64BIT_TIME")
|
||||
# The prefs file finds stored preferences by key, so key migration is possible
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -14,6 +15,8 @@ from esphome.const import (
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import host_ns
|
||||
|
||||
@@ -22,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
def _translate_pin(value: Any) -> int | str:
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -41,7 +44,7 @@ def _translate_pin(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
def validate_gpio_pin(value: Any) -> int | str:
|
||||
return _translate_pin(value)
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@ HOST_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA)
|
||||
async def host_pin_to_code(config):
|
||||
async def host_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(num))
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
@@ -14,7 +15,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await time_.register_time(var, config)
|
||||
|
||||
@@ -64,8 +64,9 @@ void OtaHttpRequestComponent::flash() {
|
||||
}
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
|
||||
if (this->update_started_) {
|
||||
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container,
|
||||
bool abort_backend) {
|
||||
if (abort_backend) {
|
||||
ESP_LOGV(TAG, "Aborting OTA backend");
|
||||
backend->abort();
|
||||
}
|
||||
@@ -106,7 +107,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
auto error_code = backend->begin(container->content_length);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "backend->begin error: %d", error_code);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
// Nothing to abort: begin() failed, so no OTA handle was opened
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/false);
|
||||
return error_code;
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
|
||||
}
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
|
||||
@@ -150,14 +152,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
md5_receive.add(buf, bufsize_or_error);
|
||||
|
||||
// write bytes to OTA backend
|
||||
this->update_started_ = true;
|
||||
error_code = backend->write(buf, bufsize_or_error);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
container->get_bytes_read() - bufsize_or_error, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return error_code;
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
this->md5_computed_ = md5_receive_str;
|
||||
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
|
||||
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
} else {
|
||||
backend->set_update_md5(md5_receive_str);
|
||||
@@ -197,7 +198,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
error_code = backend->end();
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return error_code;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
|
||||
void flash();
|
||||
|
||||
protected:
|
||||
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container);
|
||||
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container, bool abort_backend);
|
||||
uint8_t do_ota_();
|
||||
std::string get_url_with_auth_(const std::string &url);
|
||||
bool http_get_md5_();
|
||||
@@ -51,7 +51,6 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
|
||||
std::string username_{};
|
||||
std::string url_{};
|
||||
int status_ = -1;
|
||||
bool update_started_ = false;
|
||||
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -52,9 +53,10 @@ from esphome.const import (
|
||||
PLATFORM_RP2,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -96,13 +98,13 @@ CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
def validate_device(value):
|
||||
def validate_device(value: str) -> str:
|
||||
if not re.match(r"^/(?:[^/]+/)*[^/]+$", value):
|
||||
raise cv.Invalid("Device must be an absolute device path (e.g., /dev/i2c-0)")
|
||||
return value
|
||||
|
||||
|
||||
def _bus_declare_type(value):
|
||||
def _bus_declare_type(value: Any) -> ID:
|
||||
if CORE.is_esp32:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.using_arduino:
|
||||
@@ -114,7 +116,7 @@ def _bus_declare_type(value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _rp2040_i2c_controller(pin):
|
||||
def _rp2040_i2c_controller(pin: int) -> int:
|
||||
"""Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin.
|
||||
|
||||
See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"):
|
||||
@@ -125,7 +127,7 @@ def _rp2040_i2c_controller(pin):
|
||||
return (pin // 2) % 2
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_esp32:
|
||||
return cv.require_framework_version(
|
||||
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
|
||||
@@ -142,7 +144,7 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_host_config(config):
|
||||
def validate_host_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_host:
|
||||
# Host I2C is currently only supported on Linux
|
||||
if not sys.platform.lower().startswith("linux"):
|
||||
@@ -229,7 +231,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()[CONF_I2C]
|
||||
if CORE.using_zephyr and len(full_config) > 1:
|
||||
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
|
||||
@@ -281,7 +283,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.BUS)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(i2c_ns.using)
|
||||
cg.add_define("USE_I2C")
|
||||
if CORE.is_esp32:
|
||||
@@ -358,7 +360,7 @@ async def to_code(config):
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
|
||||
|
||||
def i2c_device_schema(default_address):
|
||||
def i2c_device_schema(default_address: int | None) -> cv.Schema:
|
||||
"""Create a schema for a i2c device.
|
||||
|
||||
:param default_address: The default address of the i2c device, can be None to represent
|
||||
@@ -375,7 +377,7 @@ def i2c_device_schema(default_address):
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
async def register_i2c_device(var, config):
|
||||
async def register_i2c_device(var: MockObj, config: ConfigType) -> None:
|
||||
"""Register an i2c device with the given config.
|
||||
|
||||
Sets the i2c bus to use and the i2c address.
|
||||
@@ -390,11 +392,11 @@ async def register_i2c_device(var, config):
|
||||
def final_validate_device_schema(
|
||||
name: str,
|
||||
*,
|
||||
min_frequency: cv.frequency = None,
|
||||
max_frequency: cv.frequency = None,
|
||||
min_timeout: cv.time_period = None,
|
||||
max_timeout: cv.time_period = None,
|
||||
):
|
||||
min_frequency: Any = None,
|
||||
max_frequency: Any = None,
|
||||
min_timeout: Any = None,
|
||||
max_timeout: Any = None,
|
||||
) -> cv.Schema:
|
||||
hub_schema = {}
|
||||
if (min_frequency is not None) and (max_frequency is not None):
|
||||
hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range(
|
||||
|
||||
@@ -75,8 +75,6 @@ void Infrared::dump_config() {
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
}
|
||||
|
||||
InfraredCall Infrared::make_call() { return InfraredCall(this); }
|
||||
|
||||
void Infrared::control(const InfraredCall &call) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
|
||||
@@ -134,7 +134,7 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
|
||||
const InfraredTraits &get_traits() const { return this->traits_; }
|
||||
|
||||
/// Create a call object for transmitting
|
||||
InfraredCall make_call();
|
||||
InfraredCall make_call() { return InfraredCall(this); }
|
||||
|
||||
/// Get capability flags for this infrared instance
|
||||
uint32_t get_capability_flags() const;
|
||||
|
||||
@@ -13,8 +13,6 @@ ESPColorView ESPRangeView::operator[](int32_t index) const {
|
||||
index = interpret_index(index, this->size()) + this->begin_;
|
||||
return (*this->parent_)[index];
|
||||
}
|
||||
ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; }
|
||||
ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; }
|
||||
|
||||
void ESPRangeView::set(const Color &color) {
|
||||
for (int32_t i = this->begin_; i < this->end_; i++) {
|
||||
|
||||
@@ -75,4 +75,7 @@ class ESPRangeIterator {
|
||||
int32_t i_;
|
||||
};
|
||||
|
||||
inline ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; }
|
||||
inline ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; }
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -157,8 +157,6 @@ void LightState::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
void LightState::publish_state() {
|
||||
if (this->remote_values_listeners_) {
|
||||
for (auto *listener : *this->remote_values_listeners_) {
|
||||
@@ -194,25 +192,11 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen
|
||||
this->target_state_reached_listeners_->push_back(listener);
|
||||
}
|
||||
|
||||
void LightState::set_default_transition_length(uint32_t default_transition_length) {
|
||||
this->default_transition_length_ = default_transition_length;
|
||||
}
|
||||
uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
|
||||
void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
|
||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_ = effects;
|
||||
}
|
||||
|
||||
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
void LightState::current_values_as_brightness(float *brightness) {
|
||||
this->current_values.as_brightness(brightness);
|
||||
*brightness = this->gamma_correct_lut(*brightness);
|
||||
@@ -333,8 +317,6 @@ float LightState::gamma_uncorrect_lut(float value) const {
|
||||
}
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
|
||||
|
||||
void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->stop_effect_();
|
||||
if (effect_index == 0)
|
||||
|
||||
@@ -109,7 +109,7 @@ class LightState : public EntityBase, public Component {
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
/// Shortly after HARDWARE.
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
/** The current values of the light as outputted to the light.
|
||||
*
|
||||
@@ -157,15 +157,19 @@ class LightState : public EntityBase, public Component {
|
||||
void add_target_state_reached_listener(LightTargetStateReachedListener *listener);
|
||||
|
||||
/// Set the default transition length, i.e. the transition length when no transition is provided.
|
||||
void set_default_transition_length(uint32_t default_transition_length);
|
||||
uint32_t get_default_transition_length() const;
|
||||
void set_default_transition_length(uint32_t default_transition_length) {
|
||||
this->default_transition_length_ = default_transition_length;
|
||||
}
|
||||
uint32_t get_default_transition_length() const { return this->default_transition_length_; }
|
||||
|
||||
/// Set the flash transition length
|
||||
void set_flash_transition_length(uint32_t flash_transition_length);
|
||||
uint32_t get_flash_transition_length() const;
|
||||
void set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
|
||||
/// Set the gamma correction factor
|
||||
void set_gamma_correct(float gamma_correct);
|
||||
void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
float get_gamma_correct() const { return this->gamma_correct_; }
|
||||
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
@@ -186,17 +190,17 @@ class LightState : public EntityBase, public Component {
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
/// Set the restore mode of this light
|
||||
void set_restore_mode(LightRestoreMode restore_mode);
|
||||
void set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
|
||||
/// Set a callback to populate the initial state defaults during setup.
|
||||
/// The callback is called once, then cleared. Values live in flash as code.
|
||||
void set_initial_state(void (*callback)(LightStateRTCState &));
|
||||
void set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
|
||||
|
||||
/// Return whether the light has any effects that meet the trait requirements.
|
||||
bool supports_effects();
|
||||
bool supports_effects() const { return !this->effects_.empty(); }
|
||||
|
||||
/// Get all effects for this light state.
|
||||
const FixedVector<LightEffect *> &get_effects() const;
|
||||
const FixedVector<LightEffect *> &get_effects() const { return this->effects_; }
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::initializer_list<LightEffect *> &effects);
|
||||
@@ -254,7 +258,7 @@ class LightState : public EntityBase, public Component {
|
||||
}
|
||||
|
||||
/// The result of all the current_values_as_* methods have gamma correction applied.
|
||||
void current_values_as_binary(bool *binary);
|
||||
void current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
|
||||
void current_values_as_brightness(float *brightness);
|
||||
|
||||
@@ -281,7 +285,7 @@ class LightState : public EntityBase, public Component {
|
||||
* return;
|
||||
* }
|
||||
*/
|
||||
bool is_transformer_active();
|
||||
bool is_transformer_active() const { return this->is_transformer_active_; }
|
||||
|
||||
protected:
|
||||
friend LightOutput;
|
||||
|
||||
@@ -12,13 +12,14 @@ from esphome.const import (
|
||||
CONF_ON_UNLOCK,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -102,7 +103,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
@setup_entity("lock")
|
||||
async def _setup_lock_core(var, config):
|
||||
async def _setup_lock_core(var: MockObj, config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
@@ -113,7 +114,7 @@ async def _setup_lock_core(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_lock(var, config):
|
||||
async def register_lock(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("lock", config)
|
||||
@@ -121,7 +122,7 @@ async def register_lock(var, config):
|
||||
await _setup_lock_core(var, config)
|
||||
|
||||
|
||||
async def new_lock(config, *args):
|
||||
async def new_lock(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_lock(var, config)
|
||||
return var
|
||||
@@ -143,23 +144,38 @@ LOCK_ACTION_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def lock_action_to_code(config, action_id, template_arg, args):
|
||||
async def lock_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA)
|
||||
async def lock_is_on_to_code(config, condition_id, template_arg, args):
|
||||
async def lock_is_on_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA)
|
||||
async def lock_is_off_to_code(config, condition_id, template_arg, args):
|
||||
async def lock_is_off_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(lock_ns.using)
|
||||
|
||||
@@ -201,17 +201,10 @@ void Logger::process_messages_() {
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
}
|
||||
|
||||
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||
#endif
|
||||
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
|
||||
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class Logger final : public Component {
|
||||
void loop() override;
|
||||
#endif
|
||||
/// Manually set the baud rate for serial, set to 0 to disable.
|
||||
void set_baud_rate(uint32_t baud_rate);
|
||||
void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
@@ -163,7 +163,7 @@ class Logger final : public Component {
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
||||
/// Get the UART used by the logger.
|
||||
UARTSelection get_uart() const;
|
||||
UARTSelection get_uart() const { return this->uart_; }
|
||||
#endif
|
||||
|
||||
/// Set the default log level for this logger.
|
||||
@@ -197,7 +197,7 @@ class Logger final : public Component {
|
||||
void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); }
|
||||
#endif
|
||||
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.const import (
|
||||
CONF_ON_STATE,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
)
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj
|
||||
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(
|
||||
CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s"
|
||||
): cv.update_interval,
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VANE): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
@@ -114,6 +116,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_use_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||
if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE):
|
||||
cg.add_global(mitsubishi_ns.using)
|
||||
for conf in on_state:
|
||||
|
||||
@@ -83,6 +83,7 @@ class MitsubishiCN105 {
|
||||
return this->is_telemetry_polling_enabled() ? !std::isnan(this->status_.room_temperature)
|
||||
: !std::isnan(this->status_.target_temperature);
|
||||
}
|
||||
bool is_temperature_encoding_b() const { return this->property_context_.use_temperature_encoding_b; }
|
||||
|
||||
void set_power(bool power_on);
|
||||
void set_target_temperature(float target_temperature);
|
||||
|
||||
@@ -50,7 +50,11 @@ static constexpr std::optional<Left> reverse_map_lookup(const std::array<std::pa
|
||||
return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt;
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::dump_config() { LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); }
|
||||
void MitsubishiCN105Climate::dump_config() {
|
||||
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Temperature unit: °%c",
|
||||
this->parent_->get_temperature_mapping().get_use_fahrenheit() ? 'F' : 'C');
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::setup() {
|
||||
this->parent_->add_on_status_callback([this]() { this->apply_values_(); });
|
||||
@@ -72,13 +76,15 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
|
||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
const bool use_fahrenheit = this->parent_->get_temperature_mapping().get_use_fahrenheit();
|
||||
traits.set_temperature_unit(use_fahrenheit ? TemperatureUnit::FAHRENHEIT : TemperatureUnit::CELSIUS);
|
||||
traits.set_visual_min_temperature(use_fahrenheit ? 61.0f : 16.0f);
|
||||
traits.set_visual_max_temperature(use_fahrenheit ? 88.0f : 31.0f);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
|
||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
traits.set_visual_current_temperature_step(use_fahrenheit ? 1.0f : 0.5f);
|
||||
}
|
||||
|
||||
return traits;
|
||||
@@ -86,7 +92,7 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
|
||||
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
if (const auto target_temperature = call.get_target_temperature()) {
|
||||
this->parent_->set_target_temperature(*target_temperature);
|
||||
this->parent_->set_target_temperature(this->parent_->get_temperature_mapping().to_mitsubishi(*target_temperature));
|
||||
}
|
||||
|
||||
if (const auto mode = call.get_mode()) {
|
||||
@@ -139,10 +145,10 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
void MitsubishiCN105Climate::apply_values_() {
|
||||
const auto &status = this->parent_->status();
|
||||
|
||||
this->target_temperature = status.target_temperature;
|
||||
this->target_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.target_temperature);
|
||||
|
||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||
this->current_temperature = status.room_temperature;
|
||||
this->current_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.room_temperature);
|
||||
}
|
||||
|
||||
if (status.power_on) {
|
||||
|
||||
@@ -27,6 +27,13 @@ void MitsubishiCN105Component::setup() { this->hp_.initialize(); }
|
||||
|
||||
void MitsubishiCN105Component::loop() {
|
||||
if (this->hp_.update()) {
|
||||
// Encoding A only supports whole °C values and cannot represent native °F setpoints accurately.
|
||||
// See https://github.com/esphome/esphome/pull/15488#issuecomment-5268304343
|
||||
if (this->temperature_mapping_.get_use_fahrenheit() && !this->hp_.is_temperature_encoding_b()) {
|
||||
ESP_LOGE(TAG, "Unit reports encoding A, which cannot accurately convert °F setpoints; disable 'use_fahrenheit'");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->notify_status_listeners_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,43 @@
|
||||
#include "mitsubishi_cn105.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
struct TemperatureMapping {
|
||||
float to_mitsubishi(float value) const {
|
||||
if (!this->use_fahrenheit_) {
|
||||
return value;
|
||||
}
|
||||
const int fahrenheit = std::clamp(static_cast<int>(std::round(value)), 61, 88);
|
||||
return 0.5f * (fahrenheit - 28 + (fahrenheit > 68) - (fahrenheit < 68));
|
||||
}
|
||||
|
||||
float from_mitsubishi(float value) const {
|
||||
if (!this->use_fahrenheit_) {
|
||||
return value;
|
||||
}
|
||||
if (value < 16.0f || value > 30.5f) {
|
||||
return celsius_to_fahrenheit(value);
|
||||
}
|
||||
const int mitsubishi_half_degrees = static_cast<int>(std::round(value * 2.0f));
|
||||
return mitsubishi_half_degrees + 29 - (mitsubishi_half_degrees >= 40) - (mitsubishi_half_degrees > 40);
|
||||
}
|
||||
|
||||
bool get_use_fahrenheit() const { return this->use_fahrenheit_; }
|
||||
void set_use_fahrenheit(bool value) { this->use_fahrenheit_ = value; }
|
||||
|
||||
protected:
|
||||
bool use_fahrenheit_{false};
|
||||
};
|
||||
|
||||
enum VerticalVaneMode : uint8_t {
|
||||
VERTICAL_VANE_MODE_AUTO = static_cast<uint8_t>(MitsubishiCN105::VaneMode::AUTO),
|
||||
VERTICAL_VANE_MODE_POSITION_1 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_1),
|
||||
@@ -60,6 +90,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
|
||||
void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); }
|
||||
void set_telemetry_request_min_interval(uint32_t ms) { this->hp_.set_telemetry_request_min_interval(ms); }
|
||||
void set_use_fahrenheit(bool value) { this->temperature_mapping_.set_use_fahrenheit(value); }
|
||||
|
||||
void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); }
|
||||
void clear_remote_temperature() { this->hp_.clear_remote_temperature(); }
|
||||
@@ -75,6 +106,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
const MitsubishiCN105::Status &status() const { return this->hp_.status(); }
|
||||
bool is_status_initialized() const { return this->hp_.is_status_initialized(); }
|
||||
bool is_telemetry_polling_enabled() const { return this->hp_.is_telemetry_polling_enabled(); }
|
||||
const TemperatureMapping &get_temperature_mapping() const { return this->temperature_mapping_; }
|
||||
|
||||
template<typename F> void add_on_status_callback(F &&callback) {
|
||||
this->status_callback_.add(std::forward<F>(callback));
|
||||
@@ -99,6 +131,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
}
|
||||
|
||||
MitsubishiCN105 hp_;
|
||||
TemperatureMapping temperature_mapping_;
|
||||
CallbackManager<void()> status_callback_;
|
||||
LazyCallbackManager<void(const VaneState &)> vane_state_callback_;
|
||||
};
|
||||
|
||||
@@ -618,9 +618,6 @@ class ModbusClientDevice {
|
||||
inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); }
|
||||
inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); }
|
||||
|
||||
// If more than one device is connected block sending a new command before a response is received
|
||||
ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
bool waiting_for_response() { return !this->ready_for_immediate_send(); }
|
||||
bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
|
||||
|
||||
protected:
|
||||
|
||||
@@ -668,9 +668,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string
|
||||
// Setters
|
||||
void MQTTClientComponent::disable_log_message() { this->log_message_.topic = ""; }
|
||||
bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_message_.topic.empty(); }
|
||||
void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); }
|
||||
void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
|
||||
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
|
||||
@@ -683,10 +681,6 @@ void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, cons
|
||||
}
|
||||
}
|
||||
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
|
||||
this->publish_nan_as_none_ = publish_nan_as_none;
|
||||
}
|
||||
bool MQTTClientComponent::is_publish_nan_as_none() const { return this->publish_nan_as_none_; }
|
||||
void MQTTClientComponent::disable_birth_message() {
|
||||
this->birth_message_.topic = "";
|
||||
this->recalculate_availability_();
|
||||
@@ -766,8 +760,6 @@ MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-
|
||||
|
||||
// MQTTMessageTrigger
|
||||
MQTTMessageTrigger::MQTTMessageTrigger(std::string topic) : topic_(std::move(topic)) {}
|
||||
void MQTTMessageTrigger::set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
void MQTTMessageTrigger::set_payload(const std::string &payload) { this->payload_ = payload; }
|
||||
void MQTTMessageTrigger::setup() {
|
||||
global_mqtt_client->subscribe(
|
||||
this->topic_,
|
||||
|
||||
@@ -159,7 +159,7 @@ class MQTTClientComponent final : public Component {
|
||||
|
||||
/// Manually set the topic used for logging.
|
||||
void set_log_message_template(MQTTMessage &&message);
|
||||
void set_log_level(int level);
|
||||
void set_log_level(int level) { this->log_level_ = level; }
|
||||
/// Get the topic used for logging. Defaults to "<topic_prefix>/debug" and the value is cached for speed.
|
||||
void disable_log_message();
|
||||
bool is_log_message_enabled() const;
|
||||
@@ -241,7 +241,7 @@ class MQTTClientComponent final : public Component {
|
||||
|
||||
void check_connected();
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
void register_mqtt_component(MQTTComponent *component);
|
||||
|
||||
@@ -262,8 +262,8 @@ class MQTTClientComponent final : public Component {
|
||||
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
|
||||
|
||||
// Publish None state instead of NaN for Home Assistant
|
||||
void set_publish_nan_as_none(bool publish_nan_as_none);
|
||||
bool is_publish_nan_as_none() const;
|
||||
void set_publish_nan_as_none(bool publish_nan_as_none) { this->publish_nan_as_none_ = publish_nan_as_none; }
|
||||
bool is_publish_nan_as_none() const { return this->publish_nan_as_none_; }
|
||||
|
||||
void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; }
|
||||
|
||||
@@ -344,8 +344,8 @@ class MQTTMessageTrigger final : public Trigger<std::string>, public Component {
|
||||
public:
|
||||
explicit MQTTMessageTrigger(std::string topic);
|
||||
|
||||
void set_qos(uint8_t qos);
|
||||
void set_payload(const std::string &payload);
|
||||
void set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
void set_payload(const std::string &payload) { this->payload_ = payload; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -118,8 +118,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f;
|
||||
// current_temp_step
|
||||
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
|
||||
// temperature units are always coerced to Celsius internally
|
||||
root[MQTT_TEMPERATURE_UNIT] = "C";
|
||||
root[MQTT_TEMPERATURE_UNIT] = traits.get_temperature_unit() == TemperatureUnit::FAHRENHEIT ? "F" : "C";
|
||||
|
||||
// min_humidity
|
||||
root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
|
||||
|
||||
@@ -340,10 +340,6 @@ bool MQTTComponent::send_discovery_() {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
||||
|
||||
bool MQTTComponent::get_retain() const { return this->retain_; }
|
||||
|
||||
bool MQTTComponent::is_discovery_enabled() const {
|
||||
return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
|
||||
}
|
||||
|
||||
@@ -108,11 +108,11 @@ class MQTTComponent : public Component {
|
||||
|
||||
/// Set QOS for state messages.
|
||||
void set_qos(uint8_t qos);
|
||||
uint8_t get_qos() const;
|
||||
uint8_t get_qos() const { return this->qos_; }
|
||||
|
||||
/// Set whether state message should be retained.
|
||||
void set_retain(bool retain);
|
||||
bool get_retain() const;
|
||||
bool get_retain() const { return this->retain_; }
|
||||
|
||||
/// Disable discovery. Sets friendly name to "".
|
||||
void disable_discovery();
|
||||
|
||||
@@ -39,8 +39,6 @@ uint32_t MQTTSensorComponent::get_expire_after() const {
|
||||
return *this->expire_after_;
|
||||
return 0;
|
||||
}
|
||||
void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; }
|
||||
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
|
||||
@@ -22,9 +22,9 @@ class MQTTSensorComponent final : public mqtt::MQTTComponent {
|
||||
explicit MQTTSensorComponent(sensor::Sensor *sensor);
|
||||
|
||||
/// Setup an expiry, 0 disables it
|
||||
void set_expire_after(uint32_t expire_after);
|
||||
void set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; }
|
||||
/// Disable Home Assistant value expiry.
|
||||
void disable_expire_after();
|
||||
void disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import base64
|
||||
import binascii
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_KEY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
noise_ns = cg.esphome_ns.namespace("noise")
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
def validate_encryption_key(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
|
||||
# Return original data for roundtrip conversion
|
||||
return value
|
||||
|
||||
|
||||
def decode_encryption_key(value: str) -> bytes:
|
||||
"""Decode a base64 encryption key to its 32 raw bytes.
|
||||
|
||||
a2b_base64 matches the decode the clients use (aioesphomeapi
|
||||
decode_noise_psk), so both ends derive the same bytes. The length is
|
||||
re-checked so a caller cannot turn an unvalidated short decode into a
|
||||
zero-padded PSK.
|
||||
"""
|
||||
try:
|
||||
decoded = binascii.a2b_base64(value)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
return decoded
|
||||
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def encryption_schema(config: ConfigType | None) -> ConfigType:
|
||||
# A bare `encryption:` block is valid; a missing key means the consumer
|
||||
# falls back to its keyless behavior (api provisioning, ota inheriting
|
||||
# the api key).
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.21")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "noise.h"
|
||||
#ifdef USE_NOISE
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include <noise/protocol.h>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
static const char *const TAG = "noise";
|
||||
|
||||
const LogString *noise_err_to_logstr(int err) {
|
||||
if (err == NOISE_ERROR_NO_MEMORY)
|
||||
return LOG_STR("NO_MEMORY");
|
||||
if (err == NOISE_ERROR_UNKNOWN_ID)
|
||||
return LOG_STR("UNKNOWN_ID");
|
||||
if (err == NOISE_ERROR_UNKNOWN_NAME)
|
||||
return LOG_STR("UNKNOWN_NAME");
|
||||
if (err == NOISE_ERROR_MAC_FAILURE)
|
||||
return LOG_STR("MAC_FAILURE");
|
||||
if (err == NOISE_ERROR_NOT_APPLICABLE)
|
||||
return LOG_STR("NOT_APPLICABLE");
|
||||
if (err == NOISE_ERROR_SYSTEM)
|
||||
return LOG_STR("SYSTEM");
|
||||
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
|
||||
return LOG_STR("REMOTE_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
|
||||
return LOG_STR("LOCAL_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_PSK_REQUIRED)
|
||||
return LOG_STR("PSK_REQUIRED");
|
||||
if (err == NOISE_ERROR_INVALID_LENGTH)
|
||||
return LOG_STR("INVALID_LENGTH");
|
||||
if (err == NOISE_ERROR_INVALID_PARAM)
|
||||
return LOG_STR("INVALID_PARAM");
|
||||
if (err == NOISE_ERROR_INVALID_STATE)
|
||||
return LOG_STR("INVALID_STATE");
|
||||
if (err == NOISE_ERROR_INVALID_NONCE)
|
||||
return LOG_STR("INVALID_NONCE");
|
||||
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
|
||||
return LOG_STR("INVALID_PRIVATE_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
|
||||
return LOG_STR("INVALID_PUBLIC_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_FORMAT)
|
||||
return LOG_STR("INVALID_FORMAT");
|
||||
if (err == NOISE_ERROR_INVALID_SIGNATURE)
|
||||
return LOG_STR("INVALID_SIGNATURE");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
const LogString *reject_reason_for(int err) {
|
||||
return err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") : LOG_STR("Handshake error");
|
||||
}
|
||||
|
||||
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason) {
|
||||
if (capacity == 0) {
|
||||
// A caller bug; the MAC_FAILURE_PAYLOAD_SIZE static_asserts at the call
|
||||
// sites make this unreachable, kept as cheap memory safety
|
||||
ESP_LOGVV(TAG, "Reject buffer has no capacity");
|
||||
return 0;
|
||||
}
|
||||
buf[0] = HANDSHAKE_STATUS_REJECT;
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, capacity - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(buf + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
#else
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, capacity - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(buf + 1, reason_str, reason_len);
|
||||
}
|
||||
#endif
|
||||
return reason_len + 1;
|
||||
}
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NOISE
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class NoiseContext {
|
||||
public:
|
||||
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
|
||||
// doubles as the well-known provisioning PSK that unprovisioned devices
|
||||
// accept for Noise handshakes (passive-sniffing protection only, no
|
||||
// authentication). It is never a valid real key.
|
||||
static bool is_all_zeros(const psk_t &psk) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t b : psk) {
|
||||
acc |= b;
|
||||
}
|
||||
return acc == 0;
|
||||
}
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
this->has_psk_ = !is_all_zeros(psk);
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
protected:
|
||||
psk_t psk_{};
|
||||
bool has_psk_{false};
|
||||
};
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
const LogString *noise_err_to_logstr(int err);
|
||||
|
||||
// Shared wire format for the noise transports (api and ota): every frame is
|
||||
// FRAME_INDICATOR, a 16-bit big-endian payload length, then the payload.
|
||||
// Handshake payloads start with a status byte; transport payloads end with
|
||||
// the ChaCha20-Poly1305 MAC.
|
||||
static constexpr uint8_t FRAME_INDICATOR = 0x01;
|
||||
static constexpr size_t FRAME_HEADER_SIZE = 3;
|
||||
static constexpr size_t MAC_SIZE = 16;
|
||||
static constexpr size_t MAX_HANDSHAKE_SIZE = 128;
|
||||
static constexpr uint8_t HANDSHAKE_STATUS_OK = 0x00;
|
||||
static constexpr uint8_t HANDSHAKE_STATUS_REJECT = 0x01;
|
||||
|
||||
inline void write_frame_header(uint8_t *buf, uint16_t payload_len) {
|
||||
buf[0] = FRAME_INDICATOR;
|
||||
buf[1] = (uint8_t) (payload_len >> 8);
|
||||
buf[2] = (uint8_t) payload_len;
|
||||
}
|
||||
|
||||
/// Fill buf with a handshake reject payload (status byte plus the reason
|
||||
/// text, PROGMEM aware); returns the payload length. buf needs capacity for
|
||||
/// the status byte plus the truncated reason.
|
||||
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason);
|
||||
|
||||
/// Reject reason for a failed handshake read. The MAC failure string is a
|
||||
/// wire contract: clients match it to report a wrong key.
|
||||
const LogString *reject_reason_for(int err);
|
||||
|
||||
/// Payload size of the MAC failure reject, the one reason string that is a
|
||||
/// wire contract (sizeof's NUL stands in for the status byte). static_assert
|
||||
/// reject buffers against this so a wrong key report can never truncate;
|
||||
/// longer caller-supplied reasons are informational and sized by the caller.
|
||||
static constexpr size_t MAC_FAILURE_PAYLOAD_SIZE = sizeof("Handshake MAC failure");
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "noise_handshake.h"
|
||||
#ifdef USE_NOISE
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
static const char *const TAG = "noise";
|
||||
|
||||
// Log the failing noise-c call at the same verbosity the api helper used
|
||||
// before this class existed; callers only see one collapsed error code.
|
||||
#define HANDSHAKE_STEP_LOG(func_name, err_code) \
|
||||
ESP_LOGVV(TAG, "%s failed: %s", LOG_STR_ARG(LOG_STR(func_name)), LOG_STR_ARG(noise_err_to_logstr(err_code)))
|
||||
|
||||
NoiseResponderHandshake::~NoiseResponderHandshake() {
|
||||
if (this->handshake_ != nullptr) {
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) {
|
||||
if (this->handshake_ != nullptr) {
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
}
|
||||
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
|
||||
// noise_handshakestate_new_by_id copies it, so a member would waste
|
||||
// 104 bytes per connection, and a static const would sit in RAM on
|
||||
// ESP8266 (.rodata is DRAM there).
|
||||
const NoiseProtocolId nid = {
|
||||
.prefix_id = NOISE_PREFIX_STANDARD,
|
||||
.pattern_id = NOISE_PATTERN_NN,
|
||||
.modifier_ids = {NOISE_MODIFIER_PSK0},
|
||||
.dh_id = NOISE_DH_CURVE25519,
|
||||
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
|
||||
.hash_id = NOISE_HASH_SHA256,
|
||||
.hybrid_id = NOISE_DH_NONE,
|
||||
};
|
||||
|
||||
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_new_by_id", err);
|
||||
return err;
|
||||
}
|
||||
err = noise_handshakestate_set_pre_shared_key(this->handshake_, psk.data(), psk.size());
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_set_pre_shared_key", err);
|
||||
return this->fail_init_(err);
|
||||
}
|
||||
err = noise_handshakestate_set_prologue(this->handshake_, prologue, prologue_len);
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_set_prologue", err);
|
||||
return this->fail_init_(err);
|
||||
}
|
||||
err = noise_handshakestate_start(this->handshake_);
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_start", err);
|
||||
return this->fail_init_(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Release a half-initialized state so a failed init() leaves the object as
|
||||
/// if init() was never called.
|
||||
int NoiseResponderHandshake::fail_init_(int err) {
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
|
||||
NoiseResponderHandshake::Action NoiseResponderHandshake::action() const {
|
||||
if (this->handshake_ == nullptr) {
|
||||
// A caller bug: init() was never called, or split() already released the state
|
||||
ESP_LOGVV(TAG, "action() on uninitialized or split handshake");
|
||||
return Action::ACTION_FAILED;
|
||||
}
|
||||
int raw = noise_handshakestate_get_action(this->handshake_);
|
||||
switch (raw) {
|
||||
case NOISE_ACTION_READ_MESSAGE:
|
||||
return Action::ACTION_READ;
|
||||
case NOISE_ACTION_WRITE_MESSAGE:
|
||||
return Action::ACTION_WRITE;
|
||||
case NOISE_ACTION_SPLIT:
|
||||
return Action::ACTION_SPLIT;
|
||||
default:
|
||||
// Preserve the raw code in debug logs; callers only see the collapsed enum
|
||||
ESP_LOGVV(TAG, "Unexpected noise action %d", raw);
|
||||
return Action::ACTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) {
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, data, len);
|
||||
return noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::write_message(uint8_t *out, size_t capacity, size_t &out_len) {
|
||||
out_len = 0;
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, out, capacity);
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
if (err == 0)
|
||||
out_len = mbuf.size;
|
||||
return err;
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher) {
|
||||
// Defined error postcondition: noise-c leaves the out-params unwritten on
|
||||
// its early error returns, so a caller passing uninitialized locals must
|
||||
// never see garbage to free
|
||||
send_cipher = nullptr;
|
||||
recv_cipher = nullptr;
|
||||
int err = noise_handshakestate_split(this->handshake_, &send_cipher, &recv_cipher);
|
||||
if (err != 0)
|
||||
return err;
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// noise-c's only randomness source (the vendored library compiles no rand of
|
||||
// its own); HWRNG backed. Lives in this TU so every handshake consumer links
|
||||
// it and the definition can never be dropped from the archive.
|
||||
void noise_rand_bytes(void *output, size_t len) {
|
||||
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
|
||||
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
|
||||
arch_restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NOISE
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include <noise/protocol.h>
|
||||
|
||||
#include "noise.h"
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
/** Sans-IO responder side of a Noise_NNpsk0_25519_ChaChaPoly_SHA256 handshake.
|
||||
*
|
||||
* Owns only the noise-c handshake state; the caller moves the raw handshake
|
||||
* messages (no framing) over its own transport, driven by action():
|
||||
* read_message() while READ, write_message() while WRITE, then split() to
|
||||
* take ownership of the transport ciphers. All methods return a noise-c
|
||||
* error code, 0 on success. Called outside their action() step (before
|
||||
* init(), after split()) the message methods return a noise-c error rather
|
||||
* than crashing; the library checks its state argument.
|
||||
*
|
||||
* Methods are deliberately small separate functions so callers on tight
|
||||
* stacks (RP2040 core0 scratch bank) never pay for more than one branch;
|
||||
* the curve25519 step alone needs ~2KB of stack.
|
||||
*/
|
||||
class NoiseResponderHandshake {
|
||||
public:
|
||||
// The ACTION_ prefix is macro-collision safety: SDK headers #define bare
|
||||
// names like READ/WRITE, and macros expand even inside an enum class.
|
||||
enum class Action : uint8_t { ACTION_READ, ACTION_WRITE, ACTION_SPLIT, ACTION_FAILED };
|
||||
|
||||
NoiseResponderHandshake() = default;
|
||||
~NoiseResponderHandshake();
|
||||
// Owns a raw noise-c handshake state; copying would double free it
|
||||
NoiseResponderHandshake(const NoiseResponderHandshake &) = delete;
|
||||
NoiseResponderHandshake &operator=(const NoiseResponderHandshake &) = delete;
|
||||
|
||||
/// Create and start the handshake with the given PSK and prologue. A
|
||||
/// repeated call frees the previous handshake state and starts over.
|
||||
[[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len);
|
||||
/// ACTION_FAILED is the catch-all: returned before init(), after split()
|
||||
/// has released the state, and when noise-c reports a failed handshake.
|
||||
[[nodiscard]] Action action() const;
|
||||
/// Process one received handshake message. The buffer is consumed in
|
||||
/// place: noise-c decrypts into it and zeroes it before returning.
|
||||
[[nodiscard]] int read_message(uint8_t *data, size_t len);
|
||||
/// Produce the next handshake message into out; out_len receives its size
|
||||
/// and is zero on error.
|
||||
[[nodiscard]] int write_message(uint8_t *out, size_t capacity, size_t &out_len);
|
||||
/// Hand out the transport ciphers and free the handshake state. The caller
|
||||
/// owns both cipher states and must free them with noise_cipherstate_free();
|
||||
/// both are set to nullptr on error.
|
||||
[[nodiscard]] int split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher);
|
||||
|
||||
protected:
|
||||
int fail_init_(int err);
|
||||
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user