Merge branch 'platformio-pch-libretiny' into pch-strict-ci

This commit is contained in:
J. Nick Koston
2026-09-25 16:22:03 +01:00
111 changed files with 1301 additions and 977 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
+3 -3
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -443,7 +443,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1340,7 +1340,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+3 -1
View File
@@ -737,7 +737,9 @@ file does, and it is the authority when they disagree. The most useful starting
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
7. **Detection:** Look for these patterns in compiler output:
7. **Never use `new (std::nothrow)`:** On ESP-IDF exceptions are disabled, so a failed nothrow allocation aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects `std::nothrow`.
8. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
+5
View File
@@ -1342,8 +1342,12 @@ def _upload_via_native_api(
# fall back to a plaintext upload
noise_psk = None
plaintext_fallback = False
allow_plaintext_upload = False
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
noise_psk = encryption_conf.get(CONF_KEY)
allow_plaintext_upload = bool(
encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD)
)
if not noise_psk:
raise EsphomeError(
"OTA encryption is configured but no key was resolved; "
@@ -1394,6 +1398,7 @@ def _upload_via_native_api(
ota_type,
noise_psk,
plaintext_fallback=plaintext_fallback,
allow_plaintext_upload=allow_plaintext_upload,
)
-1
View File
@@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401
Application,
Component,
ComponentPtr,
Controller,
EntityBase,
EntityCategory,
ESPTime,
+2 -3
View File
@@ -81,7 +81,7 @@ def AUTO_LOAD(config: ConfigType) -> list[str]:
api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
APIServer = api_ns.class_("APIServer", cg.Component)
HomeAssistantServiceCallAction = api_ns.class_(
"HomeAssistantServiceCallAction", automation.Action
)
@@ -462,8 +462,7 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
CORE.register_controller(var)
# Request a log listener slot for API log streaming
request_log_listener()
-3
View File
@@ -5,7 +5,6 @@
#include "api_connection.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -61,8 +60,6 @@ void APIServer::socket_failed_(const LogString *msg) {
}
void APIServer::setup() {
ControllerRegistry::register_controller(this);
#ifdef USE_API_NOISE
// Always reserve the slot: flash preferences are positional on esp8266, so
// a yaml key build must keep the layout of a runtime key build
+23 -24
View File
@@ -14,7 +14,7 @@
#include "esphome/components/socket/socket.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/entity_includes.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#ifdef USE_PROVISIONING
@@ -49,8 +49,7 @@ struct SavedNoisePsk {
bool load_saved_noise_psk(noise::psk_t &out);
#endif
class APIServer final : public Component,
public Controller
class APIServer final : public Component
#ifdef USE_CAMERA
,
public camera::CameraListener
@@ -94,58 +93,58 @@ class APIServer final : public Component,
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
void on_binary_sensor_update(binary_sensor::BinarySensor *obj);
#endif
#ifdef USE_COVER
void on_cover_update(cover::Cover *obj) override;
void on_cover_update(cover::Cover *obj);
#endif
#ifdef USE_FAN
void on_fan_update(fan::Fan *obj) override;
void on_fan_update(fan::Fan *obj);
#endif
#ifdef USE_LIGHT
void on_light_update(light::LightState *obj) override;
void on_light_update(light::LightState *obj);
#endif
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj) override;
void on_sensor_update(sensor::Sensor *obj);
#endif
#ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj) override;
void on_switch_update(switch_::Switch *obj);
#endif
#ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
void on_text_sensor_update(text_sensor::TextSensor *obj);
#endif
#ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override;
void on_climate_update(climate::Climate *obj);
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj) override;
void on_number_update(number::Number *obj);
#endif
#ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override;
void on_date_update(datetime::DateEntity *obj);
#endif
#ifdef USE_DATETIME_TIME
void on_time_update(datetime::TimeEntity *obj) override;
void on_time_update(datetime::TimeEntity *obj);
#endif
#ifdef USE_DATETIME_DATETIME
void on_datetime_update(datetime::DateTimeEntity *obj) override;
void on_datetime_update(datetime::DateTimeEntity *obj);
#endif
#ifdef USE_TEXT
void on_text_update(text::Text *obj) override;
void on_text_update(text::Text *obj);
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj) override;
void on_select_update(select::Select *obj);
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
void on_lock_update(lock::Lock *obj);
#endif
#ifdef USE_VALVE
void on_valve_update(valve::Valve *obj) override;
void on_valve_update(valve::Valve *obj);
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
void on_media_player_update(media_player::MediaPlayer *obj);
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
void on_water_heater_update(water_heater::WaterHeater *obj);
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);
@@ -188,13 +187,13 @@ class APIServer final : public Component,
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj) override;
void on_event(event::Event *obj);
#endif
#ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override;
void on_update(update::UpdateEntity *obj);
#endif
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
-1
View File
@@ -4,7 +4,6 @@
#ifdef USE_API
#include "esphome/core/component.h"
#include "esphome/core/component_iterator.h"
#include "esphome/core/controller.h"
namespace esphome::api {
class APIConnection;
+4 -14
View File
@@ -16,14 +16,14 @@ from esphome.const import (
DEVICE_CLASS_RESTART,
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, 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 MockObj, MockObjClass, TemplateArgsType
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType
CODEOWNERS = ["@esphome/core"]
@@ -40,8 +40,6 @@ button_ns = cg.esphome_ns.namespace("button")
Button = button_ns.class_("Button", cg.EntityBase)
ButtonPtr = Button.operator("ptr")
PressAction = button_ns.class_("PressAction", automation.Action)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
@@ -123,17 +121,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
)
@automation.register_action(
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
automation.register_apply_action(
"button.press", BUTTON_PRESS_SCHEMA, automation.ApplyCall("press()")
)
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)
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome::button {
template<typename... Ts> class PressAction final : public Action<Ts...> {
public:
explicit PressAction(Button *button) : button_(button) {}
void play(const Ts &...x) override { this->button_->press(); }
protected:
Button *button_;
};
} // namespace esphome::button
+1
View File
@@ -23,6 +23,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_IS_WRGB = "is_wrgb"
+15 -33
View File
@@ -214,16 +214,6 @@ def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
PreventDeepSleepAction = deep_sleep_ns.class_(
"PreventDeepSleepAction",
automation.Action,
cg.Parented.template(DeepSleepComponent),
)
AllowDeepSleepAction = deep_sleep_ns.class_(
"AllowDeepSleepAction",
automation.Action,
cg.Parented.template(DeepSleepComponent),
)
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
WAKEUP_PIN_MODES = {
@@ -422,8 +412,12 @@ async def to_code(config: ConfigType) -> None:
if CONF_TOUCH_WAKEUP in config:
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
zephyr_add_prj_conf("POWEROFF", True)
if CORE.using_zephyr:
# Devices are suspended when CPU is entering a low power state
# https://github.com/nrfconnect/sdk-zephyr/blob/v3.7.99-ncs2-2/doc/services/pm/device.rst#system-managed-device-power-management
zephyr_add_prj_conf("PM_DEVICE", True)
if "zigbee" not in CORE.loaded_integrations:
zephyr_add_prj_conf("POWEROFF", True)
cg.add_define("USE_DEEP_SLEEP")
@@ -486,27 +480,15 @@ async def deep_sleep_enter_to_code(
return var
@automation.register_action(
"deep_sleep.prevent",
PreventDeepSleepAction,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
synchronous=True,
)
@automation.register_action(
"deep_sleep.allow",
AllowDeepSleepAction,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
synchronous=True,
)
async def deep_sleep_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
for _name, _call in (
("deep_sleep.prevent", "prevent_deep_sleep()"),
("deep_sleep.allow", "allow_deep_sleep()"),
):
automation.register_apply_action(
_name,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
automation.ApplyCall(_call),
)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
@@ -121,8 +121,6 @@ class Ext1WakeTrigger : public Trigger<>, public Component {
template<typename... Ts> class EnterDeepSleepAction;
template<typename... Ts> class PreventDeepSleepAction;
/** This component allows setting up the node to go into deep sleep mode to conserve battery.
*
* To set this component up, first set *when* the deep sleep should trigger using set_run_cycles
@@ -298,15 +296,4 @@ template<typename... Ts> class EnterDeepSleepAction final : public Action<Ts...>
#endif
};
template<typename... Ts>
class PreventDeepSleepAction final : public Action<Ts...>, public Parented<DeepSleepComponent> {
public:
void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); }
};
template<typename... Ts> class AllowDeepSleepAction final : public Action<Ts...>, public Parented<DeepSleepComponent> {
public:
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
};
} // namespace esphome::deep_sleep
+54 -1
View File
@@ -11,7 +11,10 @@ from typing import Any
from esphome import yaml_util
import esphome.codegen as cg
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
from esphome.components.const import (
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION,
CONF_IGNORE_NOT_FOUND,
)
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
@@ -111,6 +114,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_NVS_CACHE_IN_PSRAM = "nvs_cache_in_psram"
CONF_FLASH_CHIP = "flash_chip"
CONF_KEY_ID = "key_id"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
@@ -1595,6 +1599,29 @@ def final_validate(config) -> None:
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
)
)
if advanced.get(CONF_NVS_CACHE_IN_PSRAM):
psram_conf = full_config.get(PSRAM_DOMAIN)
if (
psram_conf is None
or psram_conf[CONF_DISABLED]
or psram_conf[CONF_IGNORE_NOT_FOUND]
):
errs.append(
cv.Invalid(
f"'{CONF_NVS_CACHE_IN_PSRAM}' requires PSRAM with 'ignore_not_found: false'",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_CACHE_IN_PSRAM],
)
)
if (
advanced.get(CONF_NVS_ENCRYPTION) is not None
or conf_fw[CONF_SDKCONFIG_OPTIONS].get("CONFIG_NVS_ENCRYPTION") == "y"
):
errs.append(
cv.Invalid(
f"'{CONF_NVS_CACHE_IN_PSRAM}' cannot be used with NVS encryption; the keys must stay in internal RAM",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_CACHE_IN_PSRAM],
)
)
final_validate_pins(full_config)
@@ -2035,6 +2062,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_NVS_CACHE_IN_PSRAM): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
@@ -2361,6 +2389,20 @@ async def _set_libc_picolibc_newlib_compat() -> None:
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _apply_nvs_cache_in_psram(explicit: bool) -> None:
"""Keep the NVS cache in PSRAM unless NVS encryption is on, however it was enabled."""
# The encrypted partition object holds the derived keys, which must stay in internal RAM
if is_idf_sdkconfig_option_enabled("CONFIG_NVS_ENCRYPTION"):
if explicit:
_LOGGER.warning(
"%s ignored: NVS encryption keeps the NVS cache in internal RAM",
CONF_NVS_CACHE_IN_PSRAM,
)
return
set_idf_sdkconfig_default("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM", True)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_certificate_bundle_sdkconfig() -> None:
"""Enable the mbedTLS certificate bundle only when something asked for it.
@@ -2913,6 +2955,17 @@ async def to_code(config):
if advanced[CONF_EXECUTE_FROM_PSRAM]:
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
# Imported here as psram imports this module
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
# Frees internal heap (the cache scales with the NVS partition) but slows NVS, so only
# where PSRAM is known to be fitted. Decided at FINAL so every way of enabling NVS
# encryption has been seen and a user's sdkconfig_options value wins.
# Unset means on; only an explicit true is worth a warning when it has to be dropped.
requested = advanced.get(CONF_NVS_CACHE_IN_PSRAM)
if requested is not False and psram_is_guaranteed():
CORE.add_job(_apply_nvs_cache_in_psram, requested is True)
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
# significant performance benefits. Our benchmarks show socket operations are
+36 -9
View File
@@ -2,7 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components.noise import (
encryption_schema,
ENCRYPTION_SCHEMA,
new_psk_progmem,
static_encryption_key,
)
@@ -27,6 +27,7 @@ from esphome.const import (
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.espota2 import CONF_ALLOW_PLAINTEXT_UPLOAD
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -231,15 +232,38 @@ def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -
encryption_conf[CONF_KEY] = api_key
# Uploader side options live only on the ota block; the api block keeps the
# shared schema
_ENCRYPTION_SCHEMA = ENCRYPTION_SCHEMA.extend(
{
cv.Optional(CONF_ALLOW_PLAINTEXT_UPLOAD): cv.boolean,
}
)
def _encryption_schema(config: ConfigType | None) -> ConfigType:
# Only a bare `encryption:` block is keyless; `false` or a list must fail
return _ENCRYPTION_SCHEMA({} if config is None else config)
# Also called 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
if (
CONF_PASSWORD not in config
or (encryption := config.get(CONF_ENCRYPTION)) is None
):
return config
# The migration install may still have to answer the old firmware's
# password prompt on the plaintext leg; the password is not built in
if encryption.get(CONF_ALLOW_PLAINTEXT_UPLOAD):
return config
raise cv.Invalid(
f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the "
f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}' "
f"(or set '{CONF_ALLOW_PLAINTEXT_UPLOAD}: true' for the one install that "
f"migrates a device still asking for it)"
)
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
@@ -269,7 +293,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_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"
),
@@ -304,7 +328,10 @@ async def to_code(config: ConfigType) -> None:
# An empty password opts in to the auth code path so set_auth_password() can be
# called at runtime (e.g. to rotate the password from a lambda). When `password:`
# is omitted entirely, the auth path is excluded to save flash on small devices.
if CONF_PASSWORD in config:
# A password is never built in next to encryption: validation only lets
# the two coexist for the migration install, where the password answers
# the running firmware and the build is authenticated by the key
if CONF_PASSWORD in config and CONF_ENCRYPTION not in config:
cg.add_define("USE_OTA_PASSWORD")
if config[CONF_PASSWORD]:
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
+29 -2
View File
@@ -467,6 +467,8 @@ void ESPHomeOTAComponent::handle_data_() {
if (this->extended_proto_()) {
// Read ota type, 1 byte
if (!this->data_readall_(buf, 1)) {
if (this->client_left_before_start_())
return;
this->log_read_error_(LOG_STR("OTA type"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -476,6 +478,9 @@ void ESPHomeOTAComponent::handle_data_() {
// Read size, 4 bytes MSB first
if (!this->data_readall_(buf, 4)) {
// The first request byte is the type on the extended protocol; a close after it was a cut-off request
if (!this->extended_proto_() && this->client_left_before_start_())
return;
this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -542,6 +547,8 @@ void ESPHomeOTAComponent::handle_data_() {
// there is no would-block retry here and failures are already logged.
read = this->noise_read_data_(buf, requested);
if (read <= 0) {
if (this->remote_closed_)
this->log_remote_closed_(LOG_STR("data"));
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -666,7 +673,11 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
return false;
}
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
// A partial message is a cut-off request, not a clean close; the caller reports the clean one
this->remote_closed_ = at == 0;
if (at > 0) {
ESP_LOGW(TAG, "Remote closed after %u of %zu bytes", (unsigned) at, len);
}
return false;
} else {
at += read;
@@ -712,7 +723,22 @@ void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
}
void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); }
bool ESPHomeOTAComponent::client_left_before_start_() {
// Key probes and scanners hang up right after the handshake; nothing started, so no error status or callback
if (!this->remote_closed_)
return false;
ESP_LOGD(TAG, "Client left after the handshake");
this->cleanup_connection_();
return true;
}
void ESPHomeOTAComponent::log_read_error_(const LogString *what) {
if (this->remote_closed_) {
this->log_remote_closed_(what);
return;
}
ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what));
}
void ESPHomeOTAComponent::log_start_(const LogString *phase) {
char peername[socket::SOCKADDR_STR_LEN];
@@ -793,6 +819,7 @@ void ESPHomeOTAComponent::cleanup_connection_() {
this->handshake_buf_pos_ = 0;
this->ota_state_ = OTAState::IDLE;
this->ota_features_ = 0;
this->remote_closed_ = false;
this->backend_ = nullptr;
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
@@ -134,6 +134,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
void server_failed_(const LogString *msg);
void log_socket_error_(const LogString *msg);
void log_read_error_(const LogString *what);
bool client_left_before_start_();
void log_start_(const LogString *phase);
void log_remote_closed_(const LogString *during);
void cleanup_connection_();
@@ -186,6 +187,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
OTAState ota_state_{OTAState::IDLE};
uint8_t handshake_buf_pos_{0};
uint8_t ota_features_{0};
bool remote_closed_{false}; // the peer hung up cleanly during a blocking read
#ifdef USE_OTA_PASSWORD
uint8_t auth_buf_pos_{0};
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
+2 -17
View File
@@ -61,8 +61,6 @@ RESTORE_MODES = {
}
# Actions
TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action)
FanStateTrigger = fan_ns.class_(
@@ -310,21 +308,8 @@ FAN_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action(
"fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True
)
async def fan_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True
)
async def fan_turn_off_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
automation.register_apply_action("fan.toggle", FAN_ACTION_SCHEMA, call="toggle")
automation.register_apply_action("fan.turn_off", FAN_ACTION_SCHEMA, call="turn_off")
automation.register_apply_action(
"fan.turn_on",
-18
View File
@@ -6,24 +6,6 @@
namespace esphome::fan {
template<typename... Ts> class TurnOffAction final : public Action<Ts...> {
public:
explicit TurnOffAction(Fan *state) : state_(state) {}
void play(const Ts &...x) override { this->state_->turn_off().perform(); }
Fan *state_;
};
template<typename... Ts> class ToggleAction final : public Action<Ts...> {
public:
explicit ToggleAction(Fan *state) : state_(state) {}
void play(const Ts &...x) override { this->state_->toggle().perform(); }
Fan *state_;
};
template<typename... Ts> class CycleSpeedAction final : public Action<Ts...> {
public:
explicit CycleSpeedAction(Fan *state) : state_(state) {}
+10 -22
View File
@@ -12,13 +12,13 @@ from esphome.const import (
CONF_ON_UNLOCK,
CONF_WEB_SERVER,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType
CODEOWNERS = ["@esphome/core"]
@@ -29,9 +29,6 @@ Lock = lock_ns.class_("Lock", cg.EntityBase)
LockPtr = Lock.operator("ptr")
LockCall = lock_ns.class_("LockCall")
UnlockAction = lock_ns.class_("UnlockAction", automation.Action)
LockAction = lock_ns.class_("LockAction", automation.Action)
OpenAction = lock_ns.class_("OpenAction", automation.Action)
LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action)
LockStateForwarder = lock_ns.class_("LockStateForwarder")
@@ -134,23 +131,14 @@ LOCK_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action(
"lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"lock.lock", LockAction, LOCK_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True
)
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)
for _name, _call in (
("lock.unlock", "unlock()"),
("lock.lock", "lock()"),
("lock.open", "open()"),
):
automation.register_apply_action(
_name, LOCK_ACTION_SCHEMA, automation.ApplyCall(_call)
)
automation.register_apply_condition(
-30
View File
@@ -6,36 +6,6 @@
namespace esphome::lock {
template<typename... Ts> class LockAction final : public Action<Ts...> {
public:
explicit LockAction(Lock *a_lock) : lock_(a_lock) {}
void play(const Ts &...x) override { this->lock_->lock(); }
protected:
Lock *lock_;
};
template<typename... Ts> class UnlockAction final : public Action<Ts...> {
public:
explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {}
void play(const Ts &...x) override { this->lock_->unlock(); }
protected:
Lock *lock_;
};
template<typename... Ts> class OpenAction final : public Action<Ts...> {
public:
explicit OpenAction(Lock *a_lock) : lock_(a_lock) {}
void play(const Ts &...x) override { this->lock_->open(); }
protected:
Lock *lock_;
};
/// Callback forwarder that triggers an Automation<> only when a specific lock state is entered.
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
template<LockState State> struct LockStateForwarder {
+8 -9
View File
@@ -18,19 +18,18 @@ void MCP3204::dump_config() {
}
float MCP3204::read_data(uint8_t pin, bool differential) {
uint8_t command, b0, b1;
command = (1 << 6) | // start bit
((differential ? 0 : 1) << 5) | // single or differential bit
((pin & 0x07) << 2); // pin
const uint8_t command = (1 << 6) | // start bit
((differential ? 0 : 1) << 5) | // single or differential bit
((pin & 0x07) << 2); // pin
// One full-duplex transaction: command out, 12-bit result back in bytes 1 and 2.
// Word aligned so ESP-IDF DMA uses the buffer in place; only ESP32-P4 also checks the length and bounces.
alignas(4) uint8_t buffer[3] = {command, 0x00, 0x00};
this->enable();
this->transfer_byte(command);
b0 = this->transfer_byte(0x00);
b1 = this->transfer_byte(0x00);
this->transfer_array(buffer, sizeof(buffer));
this->disable();
uint16_t digital_value = encode_uint16(b0, b1) >> 4;
uint16_t digital_value = encode_uint16(buffer[1], buffer[2]) >> 4;
return float(digital_value) / 4096.000f * this->reference_voltage_; // in V
}
+14 -36
View File
@@ -6,7 +6,6 @@ import re
from urllib.parse import urljoin
from esphome import automation, external_files, git
from esphome.automation import register_action, register_condition
from esphome.bundle import add_bundle_file
import esphome.codegen as cg
from esphome.components import esp32, microphone, ota, psram
@@ -58,17 +57,6 @@ micro_wake_word_ns = cg.esphome_ns.namespace("micro_wake_word")
MicroWakeWord = micro_wake_word_ns.class_("MicroWakeWord", cg.Component)
DisableModelAction = micro_wake_word_ns.class_("DisableModelAction", automation.Action)
EnableModelAction = micro_wake_word_ns.class_("EnableModelAction", automation.Action)
StartAction = micro_wake_word_ns.class_("StartAction", automation.Action)
StopAction = micro_wake_word_ns.class_("StopAction", automation.Action)
ModelIsEnabledCondition = micro_wake_word_ns.class_(
"ModelIsEnabledCondition", automation.Condition
)
IsRunningCondition = micro_wake_word_ns.class_(
"IsRunningCondition", automation.Condition
)
WakeWordModel = micro_wake_word_ns.class_("WakeWordModel")
@@ -619,23 +607,19 @@ async def to_code(config):
MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)})
@register_action(
automation.register_apply_action(
"micro_wake_word.start",
StartAction,
MICRO_WAKE_WORD_ACTION_SCHEMA,
synchronous=True,
automation.ApplyCall("start()"),
)
@register_action(
"micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA, synchronous=True
automation.register_apply_action(
"micro_wake_word.stop",
MICRO_WAKE_WORD_ACTION_SCHEMA,
automation.ApplyCall("stop()"),
)
@register_condition(
"micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA
automation.register_apply_condition(
"micro_wake_word.is_running", MICRO_WAKE_WORD_ACTION_SCHEMA, "is_running()"
)
async def micro_wake_word_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA = automation.maybe_simple_id(
@@ -644,24 +628,18 @@ MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA = automation.maybe_simple_id(
}
)
@register_action(
automation.register_apply_action(
"micro_wake_word.enable_model",
EnableModelAction,
MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA,
synchronous=True,
automation.ApplyCall("enable()"),
)
@register_action(
automation.register_apply_action(
"micro_wake_word.disable_model",
DisableModelAction,
MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA,
synchronous=True,
automation.ApplyCall("disable()"),
)
@register_condition(
automation.register_apply_condition(
"micro_wake_word.model_is_enabled",
ModelIsEnabledCondition,
MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA,
"is_enabled()",
)
async def model_action(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, parent)
@@ -1,54 +0,0 @@
#pragma once
#include "micro_wake_word.h"
#include "streaming_model.h"
#ifdef USE_ESP32
namespace esphome::micro_wake_word {
template<typename... Ts> class StartAction final : public Action<Ts...>, public Parented<MicroWakeWord> {
public:
void play(const Ts &...x) override { this->parent_->start(); }
};
template<typename... Ts> class StopAction final : public Action<Ts...>, public Parented<MicroWakeWord> {
public:
void play(const Ts &...x) override { this->parent_->stop(); }
};
template<typename... Ts> class IsRunningCondition final : public Condition<Ts...>, public Parented<MicroWakeWord> {
public:
bool check(const Ts &...x) override { return this->parent_->is_running(); }
};
template<typename... Ts> class EnableModelAction final : public Action<Ts...> {
public:
explicit EnableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {}
void play(const Ts &...x) override { this->wake_word_model_->enable(); }
protected:
WakeWordModel *wake_word_model_;
};
template<typename... Ts> class DisableModelAction final : public Action<Ts...> {
public:
explicit DisableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {}
void play(const Ts &...x) override { this->wake_word_model_->disable(); }
protected:
WakeWordModel *wake_word_model_;
};
template<typename... Ts> class ModelIsEnabledCondition final : public Condition<Ts...> {
public:
explicit ModelIsEnabledCondition(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {}
bool check(const Ts &...x) override { return this->wake_word_model_->is_enabled(); }
protected:
WakeWordModel *wake_word_model_;
};
} // namespace esphome::micro_wake_word
#endif
+17 -58
View File
@@ -14,9 +14,9 @@ from esphome.const import (
CONF_ON_DATA,
CONF_TRIGGER_ID,
)
from esphome.core import CORE, ID
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
AUTO_LOAD = ["audio"]
@@ -29,30 +29,11 @@ microphone_ns = cg.esphome_ns.namespace("microphone")
Microphone = microphone_ns.class_("Microphone")
MicrophoneSource = microphone_ns.class_("MicrophoneSource")
CaptureAction = microphone_ns.class_(
"CaptureAction", automation.Action, cg.Parented.template(Microphone)
)
StopCaptureAction = microphone_ns.class_(
"StopCaptureAction", automation.Action, cg.Parented.template(Microphone)
)
MuteAction = microphone_ns.class_(
"MuteAction", automation.Action, cg.Parented.template(Microphone)
)
UnmuteAction = microphone_ns.class_(
"UnmuteAction", automation.Action, cg.Parented.template(Microphone)
)
DataTrigger = microphone_ns.class_(
"DataTrigger",
automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")),
)
IsCapturingCondition = microphone_ns.class_(
"IsCapturingCondition", automation.Condition
)
IsMutedCondition = microphone_ns.class_("IsMutedCondition", automation.Condition)
async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None:
for conf in config.get(CONF_ON_DATA, []):
@@ -189,44 +170,22 @@ async def microphone_source_to_code(
return mic_source
async def microphone_action(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
for _name, _call in (
("microphone.capture", "start()"),
("microphone.stop_capture", "stop()"),
("microphone.mute", "set_mute_state(true)"),
("microphone.unmute", "set_mute_state(false)"),
):
automation.register_apply_action(
_name, MICROPHONE_ACTION_SCHEMA, automation.ApplyCall(_call)
)
automation.register_action(
"microphone.capture",
CaptureAction,
MICROPHONE_ACTION_SCHEMA,
synchronous=True,
)(microphone_action)
automation.register_action(
"microphone.stop_capture",
StopCaptureAction,
MICROPHONE_ACTION_SCHEMA,
synchronous=True,
)(microphone_action)
automation.register_action(
"microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True
)(microphone_action)
automation.register_action(
"microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True
)(microphone_action)
automation.register_condition(
"microphone.is_capturing", IsCapturingCondition, MICROPHONE_ACTION_SCHEMA
)(microphone_action)
automation.register_condition(
"microphone.is_muted", IsMutedCondition, MICROPHONE_ACTION_SCHEMA
)(microphone_action)
automation.register_apply_condition(
"microphone.is_capturing", MICROPHONE_ACTION_SCHEMA, "is_running()"
)
automation.register_apply_condition(
"microphone.is_muted", MICROPHONE_ACTION_SCHEMA, "get_mute_state()"
)
@coroutine_with_priority(CoroPriority.CORE)
@@ -7,21 +7,6 @@
namespace esphome::microphone {
template<typename... Ts> class CaptureAction final : public Action<Ts...>, public Parented<Microphone> {
void play(const Ts &...x) override { this->parent_->start(); }
};
template<typename... Ts> class StopCaptureAction final : public Action<Ts...>, public Parented<Microphone> {
void play(const Ts &...x) override { this->parent_->stop(); }
};
template<typename... Ts> class MuteAction final : public Action<Ts...>, public Parented<Microphone> {
void play(const Ts &...x) override { this->parent_->set_mute_state(true); }
};
template<typename... Ts> class UnmuteAction final : public Action<Ts...>, public Parented<Microphone> {
void play(const Ts &...x) override { this->parent_->set_mute_state(false); }
};
class DataTrigger final : public Trigger<const std::vector<uint8_t> &> {
public:
explicit DataTrigger(Microphone *mic) {
@@ -29,14 +14,4 @@ class DataTrigger final : public Trigger<const std::vector<uint8_t> &> {
}
};
template<typename... Ts> class IsCapturingCondition final : public Condition<Ts...>, public Parented<Microphone> {
public:
bool check(const Ts &...x) override { return this->parent_->is_running(); }
};
template<typename... Ts> class IsMutedCondition final : public Condition<Ts...>, public Parented<Microphone> {
public:
bool check(const Ts &...x) override { return this->parent_->get_mute_state(); }
};
} // namespace esphome::microphone
+29 -46
View File
@@ -117,10 +117,7 @@ mqtt_ns = cg.esphome_ns.namespace("mqtt")
MQTTMessage = mqtt_ns.struct("MQTTMessage")
MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason")
MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action)
MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action)
MQTTEnableAction = mqtt_ns.class_("MQTTEnableAction", automation.Action)
MQTTDisableAction = mqtt_ns.class_("MQTTDisableAction", automation.Action)
MQTTMessageTrigger = mqtt_ns.class_(
"MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component
)
@@ -503,22 +500,26 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action(
"mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA, synchronous=True
)
async def mqtt_publish_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string)
cg.add(var.set_topic(template_))
# A bare literal is ambiguous between the std::string and (const char *, size_t) publish
# overloads, so constants and inlined `return "...";` lambdas are both spelled as std::string.
def _std_string(config: ConfigType, value: str) -> str:
rendered = automation.flash_string(config, value)
return rendered if CORE.is_esp8266 else f"std::string({rendered})"
template_ = await cg.templatable(config[CONF_PAYLOAD], args, cg.std_string)
cg.add(var.set_payload(template_))
template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8)
cg.add(var.set_qos(template_))
template_ = await cg.templatable(config[CONF_RETAIN], args, cg.bool_)
cg.add(var.set_retain(template_))
return var
automation.register_apply_action(
"mqtt.publish",
MQTT_PUBLISH_ACTION_SCHEMA,
automation.ApplyCall(
"publish({}, {}, {}, {})",
(
(CONF_TOPIC, "std::string", _std_string),
(CONF_PAYLOAD, "std::string", _std_string),
(CONF_QOS, cg.uint8),
(CONF_RETAIN, cg.bool_),
),
),
)
MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema(
@@ -608,34 +609,16 @@ automation.register_apply_condition(
)
@automation.register_action(
"mqtt.enable",
MQTTEnableAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(MQTTClientComponent),
}
),
synchronous=True,
)
async def mqtt_enable_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"mqtt.disable",
MQTTDisableAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(MQTTClientComponent),
}
),
synchronous=True,
)
async def mqtt_disable_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
for _name, _call in (("mqtt.enable", "enable()"), ("mqtt.disable", "disable()")):
automation.register_apply_action(
_name,
cv.Schema(
{
cv.GenerateID(): cv.use_id(MQTTClientComponent),
}
),
automation.ApplyCall(_call),
)
_platform_filter = filter_source_files_from_platform(
-37
View File
@@ -378,23 +378,6 @@ class MQTTDisconnectTrigger final : public Trigger<MQTTClientDisconnectReason> {
}
};
template<typename... Ts> class MQTTPublishAction final : public Action<Ts...> {
public:
MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, topic)
TEMPLATABLE_VALUE(std::string, payload)
TEMPLATABLE_VALUE(uint8_t, qos)
TEMPLATABLE_VALUE(bool, retain)
void play(const Ts &...x) override {
this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...),
this->retain_.value(x...));
}
protected:
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTPublishJsonAction final : public Action<Ts...> {
public:
MQTTPublishJsonAction(MQTTClientComponent *parent) : parent_(parent) {}
@@ -417,26 +400,6 @@ template<typename... Ts> class MQTTPublishJsonAction final : public Action<Ts...
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTEnableAction final : public Action<Ts...> {
public:
MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->enable(); }
protected:
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTDisableAction final : public Action<Ts...> {
public:
MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->disable(); }
protected:
MQTTClientComponent *parent_;
};
} // namespace esphome::mqtt
#endif // USE_MQTT
@@ -95,6 +95,7 @@ NoiseResponderHandshake::Action NoiseResponderHandshake::action() const {
}
int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) {
CpuFrequencyBoost boost;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, data, len);
@@ -103,6 +104,7 @@ int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) {
int NoiseResponderHandshake::write_message(uint8_t *out, size_t capacity, size_t &out_len) {
out_len = 0;
CpuFrequencyBoost boost;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, out, capacity);
+1
View File
@@ -28,6 +28,7 @@ void DeviceFirmwareUpdate::setup() {
this->reset_pin_->digital_write(true);
} else {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
arch_feed_wdt();
App.reboot();
}
}
@@ -6,8 +6,8 @@
#include "esphome/components/web_server_base/web_server_base.h"
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/entity_includes.h"
#include "esphome/core/progmem.h"
#ifdef USE_CLIMATE
#include "esphome/core/log.h"
+4 -16
View File
@@ -10,8 +10,8 @@ from esphome.const import (
CONF_STORAGE,
KEY_PAST_SAFE_MODE,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, RawExpression, TemplateArgsType
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import RawExpression
from esphome.types import ConfigType
CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"]
@@ -22,7 +22,6 @@ CONF_ON_SAFE_MODE = "on_safe_mode"
safe_mode_ns = cg.esphome_ns.namespace("safe_mode")
SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component)
MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action)
def _remove_id_if_disabled(value: ConfigType) -> ConfigType:
@@ -53,26 +52,15 @@ CONFIG_SCHEMA = cv.All(
)
@automation.register_action(
automation.register_apply_action(
"safe_mode.mark_successful",
MarkSuccessfulAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(SafeModeComponent),
}
),
synchronous=True,
automation.ApplyCall("mark_successful()"),
)
async def safe_mode_mark_successful_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg)
cg.add(var.set_parent(parent))
return var
_CALLBACK_AUTOMATIONS = (
-12
View File
@@ -1,12 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "safe_mode.h"
namespace esphome::safe_mode {
template<typename... Ts> class MarkSuccessfulAction final : public Action<Ts...>, public Parented<SafeModeComponent> {
public:
void play(const Ts &...x) override { this->parent_->mark_successful(); }
};
} // namespace esphome::safe_mode
+2 -8
View File
@@ -9,7 +9,6 @@ CODEOWNERS = ["@esphome/core"]
script_ns = cg.esphome_ns.namespace("script")
Script = script_ns.class_("Script", automation.Trigger.template())
ScriptExecuteAction = script_ns.class_("ScriptExecuteAction", automation.Action)
ScriptStopAction = script_ns.class_("ScriptStopAction", automation.Action)
ScriptWaitAction = script_ns.class_("ScriptWaitAction", automation.Action, cg.Component)
SingleScript = script_ns.class_("SingleScript", Script)
RestartScript = script_ns.class_("RestartScript", Script)
@@ -206,16 +205,11 @@ async def script_execute_action_to_code(config, action_id, template_arg, args):
return var
@automation.register_action(
automation.register_apply_action(
"script.stop",
ScriptStopAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
synchronous=True,
automation.ApplyCall("stop()"),
)
async def script_stop_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
-10
View File
@@ -257,16 +257,6 @@ template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, T
Args args_;
};
template<class C, typename... Ts> class ScriptStopAction final : public Action<Ts...> {
public:
ScriptStopAction(C *script) : script_(script) {}
void play(const Ts &...x) override { this->script_->stop(); }
protected:
C *script_;
};
/** Wait for a script to finish before continuing.
*
* Uses queue-based storage to safely handle concurrent executions.
+4 -21
View File
@@ -21,8 +21,8 @@ from esphome.const import (
CONF_VERSION,
CONF_WIDTH,
)
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
# mdns for autodiscovery
@@ -109,13 +109,6 @@ SendspinHub = sendspin_ns.class_(
)
SendspinSwitchCommandAction = sendspin_ns.class_(
"SendspinSwitchCommandAction",
automation.Action,
cg.Parented.template(SendspinHub),
)
@dataclass
class SendspinConfiguration:
artwork_support: bool = False
@@ -240,21 +233,11 @@ SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All(
)
@automation.register_action(
automation.register_apply_action(
"sendspin.switch",
SendspinSwitchCommandAction,
SENDSPIN_SIMPLE_ACTION_SCHEMA,
synchronous=True,
automation.ApplyCall("switch_client()"),
)
async def sendspin_switch_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def to_code(config: ConfigType) -> None:
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/core/automation.h"
#include "sendspin_hub.h"
namespace esphome::sendspin_ {
#ifdef USE_SENDSPIN_CONTROLLER
template<typename... Ts> class SendspinSwitchCommandAction final : public Action<Ts...>, public Parented<SendspinHub> {
public:
void play(const Ts &...x) override {
// Clear any EXTERNAL_SOURCE state so the switch command is followed
this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED);
this->parent_->send_client_command(sendspin::SendspinControllerCommand::SWITCH);
}
};
#endif // USE_SENDSPIN_CONTROLLER
} // namespace esphome::sendspin_
#endif // USE_ESP32
+2 -20
View File
@@ -16,7 +16,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.core import ID
from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigType
from .. import (
@@ -198,16 +197,8 @@ async def to_code(config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
SendspinImageTransitionFinishedAction = sendspin_ns.class_(
"SendspinImageTransitionFinishedAction",
automation.Action,
cg.Parented.template(SendspinImageSlot),
)
@automation.register_action(
automation.register_apply_action(
"sendspin.image.transition_finished",
SendspinImageTransitionFinishedAction,
automation.maybe_simple_id(
cv.Schema(
{
@@ -215,14 +206,5 @@ SendspinImageTransitionFinishedAction = sendspin_ns.class_(
}
)
),
synchronous=True,
automation.ApplyCall("transition_finished()"),
)
async def sendspin_image_transition_finished_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> cg.MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@@ -1,20 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_SENDSPIN_ARTWORK)
#include "esphome/core/automation.h"
#include "sendspin_image.h"
namespace esphome::sendspin_ {
template<typename... Ts>
class SendspinImageTransitionFinishedAction final : public Action<Ts...>, public Parented<SendspinImageSlot> {
public:
void play(const Ts &...x) override { this->parent_->transition_finished(); }
};
} // namespace esphome::sendspin_
#endif
@@ -8,8 +8,6 @@ from esphome.const import (
CONF_SAMPLE_RATE,
CONF_TASK_STACK_IN_PSRAM,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .. import (
@@ -41,18 +39,6 @@ SendspinMediaSource = sendspin_ns.class_(
media_source.MediaSource,
)
EnableStaticDelayAdjustmentAction = sendspin_ns.class_(
"EnableStaticDelayAdjustmentAction",
automation.Action,
cg.Parented.template(SendspinMediaSource),
)
DisableStaticDelayAdjustmentAction = sendspin_ns.class_(
"DisableStaticDelayAdjustmentAction",
automation.Action,
cg.Parented.template(SendspinMediaSource),
)
def _resolve_codecs(config: ConfigType) -> ConfigType:
"""Validate the codec preference list, filling in the default when it is not set."""
@@ -144,25 +130,16 @@ SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA = automation.maybe_simple_id(
)
)
@automation.register_action(
"sendspin.media_source.enable_static_delay_adjustment",
EnableStaticDelayAdjustmentAction,
SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"sendspin.media_source.disable_static_delay_adjustment",
DisableStaticDelayAdjustmentAction,
SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA,
synchronous=True,
)
async def sendspin_static_delay_adjustment_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
for _name, _call in (
(
"sendspin.media_source.enable_static_delay_adjustment",
"set_static_delay_adjustable(true)",
),
(
"sendspin.media_source.disable_static_delay_adjustment",
"set_static_delay_adjustable(false)",
),
):
automation.register_apply_action(
_name, SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@@ -1,26 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_SENDSPIN_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
#include "esphome/core/automation.h"
#include "sendspin_media_source.h"
namespace esphome::sendspin_ {
template<typename... Ts>
class EnableStaticDelayAdjustmentAction final : public Action<Ts...>, public Parented<SendspinMediaSource> {
public:
void play(const Ts &...x) override { this->parent_->set_static_delay_adjustable(true); }
};
template<typename... Ts>
class DisableStaticDelayAdjustmentAction final : public Action<Ts...>, public Parented<SendspinMediaSource> {
public:
void play(const Ts &...x) override { this->parent_->set_static_delay_adjustable(false); }
};
} // namespace esphome::sendspin_
#endif
@@ -284,6 +284,13 @@ void SendspinHub::send_client_command(sendspin::SendspinControllerCommand comman
}
}
// THREAD CONTEXT: Main loop (invoked from the sendspin.switch action)
void SendspinHub::switch_client() {
// Clear any EXTERNAL_SOURCE state so the switch command is followed
this->update_state(sendspin::SendspinClientState::SYNCHRONIZED);
this->send_client_command(sendspin::SendspinControllerCommand::SWITCH);
}
// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) {
this->controller_state_callbacks_.call(state);
@@ -181,6 +181,9 @@ class SendspinHub final : public Component,
void send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume = std::nullopt,
std::optional<bool> mute = std::nullopt);
/// @brief Sends the SWITCH controller command; exposed as the sendspin.switch action.
void switch_client();
template<typename F> void add_controller_state_callback(F &&callback) {
this->controller_state_callbacks_.add(std::forward<F>(callback));
}
+1 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
from esphome.components.const import CONF_HUMIDITY_SOURCE
import esphome.config_validation as cv
from esphome.const import (
CONF_BASELINE,
@@ -31,7 +32,6 @@ SGP30Component = sgp30_ns.class_(
CONF_ECO2_BASELINE = "eco2_baseline"
CONF_TVOC_BASELINE = "tvoc_baseline"
CONF_UPTIME = "uptime"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONFIG_SCHEMA = (
+5 -3
View File
@@ -1,6 +1,10 @@
import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
from esphome.components.const import (
CONF_HUMIDITY_SOURCE,
CONF_NOX_INDEX,
CONF_VOC_INDEX,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
@@ -33,8 +37,6 @@ SGP4xComponent = sgp4x_ns.class_(
sensirion_common.SensirionI2CDevice,
)
CONF_HUMIDITY_SOURCE = "humidity_source"
def validate_sensors(config: ConfigType) -> ConfigType:
if CONF_VOC_INDEX not in config and CONF_NOX_INDEX not in config:
@@ -1,5 +1,6 @@
#ifdef USE_ESP8266
#include "uart_component_esp8266.h"
#include <esp8266_peri.h>
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
@@ -251,23 +252,35 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
}
// A byte can arrive while a CpuFrequencyBoost has an 80 MHz build at 160 MHz; the clock select bit doubles
// the bit time then. The whole byte is read inside the ISR, so the clock cannot change partway through.
__attribute__((always_inline)) static inline uint32_t rx_bit_time(uint32_t bit_time) {
#if F_CPU != 160000000L
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- CPU2X is MMIO at a fixed address
return bit_time << (CPU2X & 1);
#else
return bit_time;
#endif
}
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500;
const uint32_t bit_time = rx_bit_time(arg->bit_time_);
uint32_t wait = bit_time + bit_time / 3 - 500;
const uint32_t start = arch_get_cpu_cycle_count();
uint8_t rec = 0;
// Manually unroll the loop
for (int i = 0; i < arg->data_bits_; i++)
rec |= arg->read_bit_(&wait, start) << i;
rec |= arg->read_bit_(&wait, start, bit_time) << i;
/* If parity is enabled, just read it and ignore it. */
/* TODO: Should we check parity? Or is it too slow for nothing added..*/
if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD)
arg->read_bit_(&wait, start);
arg->read_bit_(&wait, start, bit_time);
// Stop bit
arg->wait_(&wait, start);
arg->wait_(&wait, start, bit_time);
if (arg->stop_bits_ == 2)
arg->wait_(&wait, start);
arg->wait_(&wait, start, bit_time);
arg->rx_buffer_[arg->rx_in_pos_] = rec;
arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_;
@@ -296,37 +309,39 @@ void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
}
{
// Transmit runs from the main loop and never overlaps a CpuFrequencyBoost
InterruptLock lock;
uint32_t wait = this->bit_time_;
const uint32_t bit_time = this->bit_time_;
uint32_t wait = bit_time;
const uint32_t start = arch_get_cpu_cycle_count();
// Start bit
this->write_bit_(false, &wait, start);
this->write_bit_(false, &wait, start, bit_time);
for (int i = 0; i < this->data_bits_; i++) {
bool bit = data & (1 << i);
this->write_bit_(bit, &wait, start);
this->write_bit_(bit, &wait, start, bit_time);
if (need_parity_bit)
parity_bit ^= bit;
}
if (need_parity_bit)
this->write_bit_(parity_bit, &wait, start);
this->write_bit_(parity_bit, &wait, start, bit_time);
// Stop bit
this->write_bit_(true, &wait, start);
this->write_bit_(true, &wait, start, bit_time);
if (this->stop_bits_ == 2)
this->wait_(&wait, start);
this->wait_(&wait, start, bit_time);
}
}
void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) {
void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
while (arch_get_cpu_cycle_count() - start < *wait)
;
*wait += this->bit_time_;
*wait += bit_time;
}
bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) {
this->wait_(wait, start);
bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
this->wait_(wait, start, bit_time);
return this->rx_pin_.digital_read();
}
void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) {
void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
this->tx_pin_.digital_write(bit);
this->wait_(wait, start);
this->wait_(wait, start, bit_time);
}
uint8_t ESP8266SoftwareSerial::read_byte() {
if (this->rx_in_pos_ == this->rx_out_pos_)
@@ -28,9 +28,9 @@ class ESP8266SoftwareSerial {
protected:
static void gpio_intr(ESP8266SoftwareSerial *arg);
void wait_(uint32_t *wait, const uint32_t &start);
bool read_bit_(uint32_t *wait, const uint32_t &start);
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start);
void wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
bool read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time);
uint32_t bit_time_{0};
uint8_t *rx_buffer_{nullptr};
+2 -3
View File
@@ -62,7 +62,7 @@ DEFAULT_PORT = 80
web_server_ns = cg.esphome_ns.namespace("web_server")
WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller)
WebServer = web_server_ns.class_("WebServer", cg.Component)
sorting_groups = {}
@@ -378,8 +378,7 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], paren)
await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
CORE.register_controller(var)
version = config[CONF_VERSION]
+5 -4
View File
@@ -5,7 +5,6 @@
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -365,7 +364,6 @@ json::SerializationBuffer<> WebServer::get_config_json() {
}
void WebServer::setup() {
ControllerRegistry::register_controller(this);
this->base_->init();
#ifdef USE_LOGGER
@@ -1415,8 +1413,11 @@ json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::st
json::JsonBuilder builder;
JsonObject root = builder.root();
const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str();
set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config);
// A password entity shows the mask and prefills the input with nothing, so the secret never
// reaches the JSON and the mask cannot be written back as the value
const bool password = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD;
set_json_icon_state_value(root, obj, "text", password ? "********" : value.c_str(), password ? "" : value.c_str(),
start_config);
root[ESPHOME_F("min_length")] = obj->traits.get_min_length();
root[ESPHOME_F("max_length")] = obj->traits.get_max_length();
root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str();
+27 -22
View File
@@ -6,8 +6,8 @@
#include "esphome/components/web_server_base/web_server_base.h"
#ifdef USE_WEBSERVER
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/entity_includes.h"
#include "esphome/core/progmem.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
@@ -187,7 +187,7 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent
* under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API
* can be found under https://esphome.io/web-api/.
*/
class WebServer final : public Controller, public Component, public AsyncWebHandler {
class WebServer final : public Component, public AsyncWebHandler {
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
friend class DeferredUpdateEventSourceList;
#endif
@@ -292,7 +292,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj) override;
void on_sensor_update(sensor::Sensor *obj);
/// Handle a sensor request under '/sensor/<id>'.
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -301,7 +301,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj) override;
void on_switch_update(switch_::Switch *obj);
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -319,7 +319,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
void on_binary_sensor_update(binary_sensor::BinarySensor *obj);
/// Handle a binary sensor request under '/binary_sensor/<id>'.
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -329,7 +329,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_FAN
void on_fan_update(fan::Fan *obj) override;
void on_fan_update(fan::Fan *obj);
/// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -339,7 +339,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_LIGHT
void on_light_update(light::LightState *obj) override;
void on_light_update(light::LightState *obj);
/// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -349,7 +349,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
void on_text_sensor_update(text_sensor::TextSensor *obj);
/// Handle a text sensor request under '/text_sensor/<id>'.
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -359,7 +359,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_COVER
void on_cover_update(cover::Cover *obj) override;
void on_cover_update(cover::Cover *obj);
/// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -369,7 +369,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj) override;
void on_number_update(number::Number *obj);
/// Handle a number request under '/number/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -378,7 +378,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override;
void on_date_update(datetime::DateEntity *obj);
/// Handle a date request under '/date/<id>'.
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -387,7 +387,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_DATETIME_TIME
void on_time_update(datetime::TimeEntity *obj) override;
void on_time_update(datetime::TimeEntity *obj);
/// Handle a time request under '/time/<id>'.
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -396,7 +396,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_DATETIME_DATETIME
void on_datetime_update(datetime::DateTimeEntity *obj) override;
void on_datetime_update(datetime::DateTimeEntity *obj);
/// Handle a datetime request under '/datetime/<id>'.
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -405,7 +405,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_TEXT
void on_text_update(text::Text *obj) override;
void on_text_update(text::Text *obj);
/// Handle a text input request under '/text/<id>'.
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -414,7 +414,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj) override;
void on_select_update(select::Select *obj);
/// Handle a select request under '/select/<id>'.
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -423,7 +423,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override;
void on_climate_update(climate::Climate *obj);
/// Handle a climate request under '/climate/<id>'.
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -432,7 +432,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
void on_lock_update(lock::Lock *obj);
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -442,7 +442,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_VALVE
void on_valve_update(valve::Valve *obj) override;
void on_valve_update(valve::Valve *obj);
/// Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -452,7 +452,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
/// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -462,7 +462,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
void on_water_heater_update(water_heater::WaterHeater *obj);
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -484,8 +484,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
static json::SerializationBuffer<> radio_frequency_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_MEDIA_PLAYER
// Not exposed over HTTP; the stub only satisfies ControllerContract
void on_media_player_update(media_player::MediaPlayer *) {}
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj) override;
void on_event(event::Event *obj);
static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source);
@@ -495,7 +500,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#endif
#ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override;
void on_update(update::UpdateEntity *obj);
/// Handle a update request under '/update/<id>'.
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
+10 -50
View File
@@ -41,16 +41,6 @@ _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$")
wireguard_ns = cg.esphome_ns.namespace("wireguard")
Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent)
AllowedIP = wireguard_ns.struct("AllowedIP")
WireguardPeerOnlineCondition = wireguard_ns.class_(
"WireguardPeerOnlineCondition", automation.Condition
)
WireguardEnabledCondition = wireguard_ns.class_(
"WireguardEnabledCondition", automation.Condition
)
WireguardEnableAction = wireguard_ns.class_("WireguardEnableAction", automation.Action)
WireguardDisableAction = wireguard_ns.class_(
"WireguardDisableAction", automation.Action
)
def _wireguard_key(value):
@@ -163,47 +153,17 @@ async def to_code(config):
await cg.register_component(var, config)
@automation.register_condition(
"wireguard.peer_online",
WireguardPeerOnlineCondition,
cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}),
WIREGUARD_AUTOMATION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)})
automation.register_apply_condition(
"wireguard.peer_online", WIREGUARD_AUTOMATION_SCHEMA, "is_peer_up()"
)
async def wireguard_peer_up_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_condition(
"wireguard.enabled",
WireguardEnabledCondition,
cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}),
automation.register_apply_condition(
"wireguard.enabled", WIREGUARD_AUTOMATION_SCHEMA, "is_enabled()"
)
async def wireguard_enabled_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"wireguard.enable",
WireguardEnableAction,
cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}),
synchronous=True,
automation.register_apply_action(
"wireguard.enable", WIREGUARD_AUTOMATION_SCHEMA, automation.ApplyCall("enable()")
)
async def wireguard_enable_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"wireguard.disable",
WireguardDisableAction,
cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}),
synchronous=True,
automation.register_apply_action(
"wireguard.disable", WIREGUARD_AUTOMATION_SCHEMA, automation.ApplyCall("disable()")
)
async def wireguard_disable_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
-26
View File
@@ -4,7 +4,6 @@
#include <ctime>
#include <initializer_list>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/time/real_time_clock.h"
@@ -164,30 +163,5 @@ static constexpr size_t MASK_KEY_BUFFER_SIZE = 12;
/// Strip most part of the key only for secure printing
void mask_key_to(char *buffer, size_t len, const char *key);
/// Condition to check if remote peer is online.
template<typename... Ts>
class WireguardPeerOnlineCondition final : public Condition<Ts...>, public Parented<Wireguard> {
public:
bool check(const Ts &...x) override { return this->parent_->is_peer_up(); }
};
/// Condition to check if Wireguard component is enabled.
template<typename... Ts> class WireguardEnabledCondition final : public Condition<Ts...>, public Parented<Wireguard> {
public:
bool check(const Ts &...x) override { return this->parent_->is_enabled(); }
};
/// Action to enable Wireguard component.
template<typename... Ts> class WireguardEnableAction final : public Action<Ts...>, public Parented<Wireguard> {
public:
void play(const Ts &...x) override { this->parent_->enable(); }
};
/// Action to disable Wireguard component.
template<typename... Ts> class WireguardDisableAction final : public Action<Ts...>, public Parented<Wireguard> {
public:
void play(const Ts &...x) override { this->parent_->disable(); }
};
} // namespace esphome::wireguard
#endif
+5 -1
View File
@@ -27,7 +27,11 @@ void arch_init() {
if (device_is_ready(WDT)) {
static wdt_timeout_cfg wdt_config{};
wdt_config.flags = WDT_FLAG_RESET_SOC;
#ifdef USE_ZIGBEE
#ifndef USE_BOOTLOADER_MCUBOOT
// Adafruit bootloader doesn't feed the WDT while
// erasing flash during a firmware update, so a shorter timeout can break the update.
wdt_config.window.max = 30000;
#elif defined(USE_ZIGBEE)
// zboss thread uses a lot of CPU cycles during startup
wdt_config.window.max = 10000;
#else
+29 -5
View File
@@ -272,7 +272,9 @@ static void ezb_task(void *pv_parameters) {
vTaskDelete(NULL);
}
ZigbeeComponent::ZigbeeComponent() {
void ZigbeeComponent::setup() {
global_zigbee = this;
esp_zigbee_platform_config_t platform_config = {
.storage_partition_name = "nvs",
.radio_config = EZB_DEFAULT_RADIO_CONFIG(),
@@ -299,11 +301,7 @@ ZigbeeComponent::ZigbeeComponent() {
this->mark_failed();
return;
}
this->dev_desc_ = ezb_af_create_device_desc();
}
void ZigbeeComponent::setup() {
global_zigbee = this;
#ifdef USE_WIFI
if (esp_coex_wifi_i154_enable() != ESP_OK) {
this->mark_failed();
@@ -341,6 +339,15 @@ void ZigbeeComponent::setup() {
};
ezb_af_set_node_power_desc(&desc);
// Finish zigbee data model
for (auto &attr_value : this->attr_values_) {
ezb_zcl_attr_desc_t attr_desc = attr_value.attr_desc;
void *value_p = &attr_value.value;
ezb_zcl_attr_desc_set_value(attr_desc, value_p);
}
// free memory
std::vector<AttrValue>().swap(this->attr_values_);
// Start the Zigbee task with priority 1 to ensure main loop can still run even if Zigbee is busy
xTaskCreate(ezb_task, "Zigbee_main", 4096, NULL, 1, NULL);
this->disable_loop(); // loop is only needed for processing events, so disable until we join a network
@@ -383,6 +390,23 @@ void ZigbeeComponent::dump_config() {
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER));
}
}
bool ZigbeeComponent::string_attr_exists_(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id) {
ezb_af_ep_desc_t ep_desc = ezb_af_device_get_endpoint_desc(this->dev_desc_, endpoint_id);
if (ep_desc == NULL) {
return false;
}
ezb_zcl_cluster_desc_t cluster_desc = ezb_af_endpoint_get_cluster_desc(ep_desc, cluster_id, role);
if (cluster_desc == NULL) {
return false;
}
if (ezb_zcl_cluster_get_attr_desc(cluster_desc, attr_id, EZB_ZCL_STD_MANUF_CODE) == NULL) {
return false;
}
ESP_LOGW(TAG, "Attribute 0x%04X already exists in endpoint %u cluster 0x%04X. Can't add new value", attr_id,
endpoint_id, cluster_id);
return true;
}
} // namespace esphome::zigbee
#endif
+33 -3
View File
@@ -30,11 +30,15 @@ static const uint32_t EZB_PRIMARY_CHANNEL_MASK = 0x07FFF800U; /* channels 11-26
uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size = false);
struct AttrValue {
ezb_zcl_attr_desc_t attr_desc;
uint8_t value[4];
};
class ZigbeeAttribute;
class ZigbeeComponent final : public Component {
public:
ZigbeeComponent();
void setup() override;
void loop() override;
void dump_config() override;
@@ -88,7 +92,8 @@ class ZigbeeComponent final : public Component {
// automations
// key tuple could be replaced by single 64 (48) bit int with bit fields for endpoint, cluster, role and attr_id
std::map<std::tuple<uint8_t, uint16_t, uint8_t, uint16_t>, ZigbeeAttribute *> attributes_;
ezb_af_device_desc_t dev_desc_;
std::vector<AttrValue> attr_values_;
ezb_af_device_desc_t dev_desc_ = ezb_af_create_device_desc();
CallbackManager<void(bool)> join_cb_{};
LazyCallbackManager<void()> start_cb_{};
bool start_reported_{false};
@@ -96,6 +101,8 @@ class ZigbeeComponent final : public Component {
std::atomic<bool> joined_ = false;
std::atomic<bool> join_pending_ = false;
std::atomic<bool> factory_new_ = false;
// TODO: remove when esp-zigbee-lib fixes set_value before init
bool string_attr_exists_(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id);
};
template<typename T>
@@ -110,10 +117,22 @@ void ZigbeeComponent::add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint1
// The size byte of the zcl_str must be set to the maximum value,
// even though the initial string may be shorter.
if constexpr (std::is_same<T, std::string>::value) {
if (this->string_attr_exists_(endpoint_id, cluster_id, role, attr_id)) {
if (attr != nullptr) {
this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr;
}
return;
}
auto zcl_str = get_zcl_string(value.c_str(), max_size, true);
add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str);
delete[] zcl_str;
} else if constexpr (std::is_convertible<T, const char *>::value) {
if (this->string_attr_exists_(endpoint_id, cluster_id, role, attr_id)) {
if (attr != nullptr) {
this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr;
}
return;
}
auto zcl_str = get_zcl_string(value, max_size, true);
add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str);
delete[] zcl_str;
@@ -133,7 +152,18 @@ void ZigbeeComponent::add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint
if (cluster_desc == NULL) {
return;
}
esphome_zb_cluster_add_or_update_attr(cluster_id, cluster_desc, attr_id, value_p);
// TODO: revert when esp-zigbee-lib fixes set_value before init
ezb_zcl_attr_desc_t attr_desc = ezb_zcl_cluster_get_attr_desc(cluster_desc, attr_id, EZB_ZCL_STD_MANUF_CODE);
if (attr_desc != NULL) {
static_assert(sizeof(*value_p) <= 4);
AttrValue attr_value;
attr_value.attr_desc = attr_desc;
memcpy(&attr_value.value, value_p, sizeof(*value_p));
attr_values_.push_back(attr_value);
} else {
esphome_zb_cluster_add_attr(cluster_id, cluster_desc, attr_id, value_p);
}
if (attr != nullptr) {
this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr;
+5 -6
View File
@@ -52,8 +52,8 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing
KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count"
# Key for the controllers (APIServer, WebServer) that receive entity state updates
KEY_CONTROLLER_REGISTRY_CONTROLLERS = "controller_registry_controllers"
# CORE.data key for the "is_rp2040 deprecation warning already fired this
# run" flag. Mirrors the ``cv.only_on_rp2040`` dedupe pattern; cleared
@@ -1209,10 +1209,9 @@ class EsphomeCore:
if not self.platform_counts[platform_name]:
self.platform_counts[platform_name] = 1
def register_controller(self) -> None:
"""Track registration of a Controller for ControllerRegistry StaticVector sizing."""
controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0)
self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1
def register_controller(self, controller: "MockObj") -> None:
"""Register a controller that receives every entity state update."""
self.data.setdefault(KEY_CONTROLLER_REGISTRY_CONTROLLERS, []).append(controller)
@property
def cpp_main_section(self):
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/entity_includes.h"
#include "esphome/core/helpers.h"
#ifdef USE_CAMERA
+18 -8
View File
@@ -46,7 +46,7 @@ from esphome.const import (
)
from esphome.core import (
CORE,
KEY_CONTROLLER_REGISTRY_COUNT,
KEY_CONTROLLER_REGISTRY_CONTROLLERS,
CoroPriority,
coroutine_with_priority,
)
@@ -680,12 +680,22 @@ async def _add_platform_defines() -> None:
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_controller_registry_define() -> None:
# Generate StaticVector size for ControllerRegistry
controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0)
if controller_count > 0:
cg.add_define("USE_CONTROLLER_REGISTRY")
cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count)
async def _add_controller_registry_dispatch() -> None:
# controller_dispatch.h defines ControllerRegistry::notify_*() as direct
# calls on the controllers returned by esphome_controllers(), emitted as
# static auto esphome_controllers() { return std::tuple{a, b}; }
controllers = CORE.data.get(KEY_CONTROLLER_REGISTRY_CONTROLLERS)
if not controllers:
return
cg.add_define("USE_CONTROLLER_REGISTRY")
controllers = cg.ArrayInitializer(*controllers)
cg.add_global(cg.RawStatement("#include <tuple>"))
cg.add_global(
cg.RawStatement(
f"static auto esphome_controllers() {{ return std::tuple{controllers}; }}"
)
)
cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"'))
@coroutine_with_priority(CoroPriority.FINAL)
@@ -761,7 +771,7 @@ async def to_code(config: ConfigType) -> None:
)
CORE.add_job(_add_platform_defines)
CORE.add_job(_add_controller_registry_define)
CORE.add_job(_add_controller_registry_dispatch)
CORE.add_job(_add_looping_components)
CORE.add_job(_add_automations, config)
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/entity_includes.h"
namespace esphome {
class Controller {
public:
// Controller virtual methods (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) virtual void on_##callback(type *obj){};
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
};
} // namespace esphome
+50
View File
@@ -0,0 +1,50 @@
#pragma once
// Included once by the generated main.cpp, after it returns the registered controllers as a tuple:
//
// static auto esphome_controllers() { return std::tuple{api_apiserver_id, web_server_webserver_id}; }
// #include "esphome/core/controller_dispatch.h"
//
// Defines ControllerRegistry::notify_*() as direct calls on those controllers. Excluded from
// esphome.h and the clang-tidy all-headers file, so nothing else includes it.
#include <tuple>
#include <type_traits>
#include "esphome/core/controller_registry.h"
namespace esphome {
// NOLINTBEGIN(bugprone-macro-parentheses)
/// A controller provides a plain on_*_update() member for every entity type in the build.
template<typename T>
concept ControllerContract = requires(T &controller) {
controller; // keeps the requirement list non-empty when no entity type has a callback
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
controller.on_##callback(static_cast<type *>(nullptr));
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
};
template<typename... Controllers> constexpr bool controllers_satisfy_contract(std::tuple<Controllers...> *) {
return (ControllerContract<std::remove_pointer_t<Controllers>> && ...);
}
static_assert(controllers_satisfy_contract(static_cast<decltype(esphome_controllers()) *>(nullptr)),
"A registered controller is missing an on_*_update() callback for an entity type in this build "
"(ControllerContract in esphome/core/controller_dispatch.h)");
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
void ControllerRegistry::notify_##callback(type *obj) { \
std::apply([obj](auto *...controller) { (controller->on_##callback(obj), ...); }, esphome_controllers()); \
}
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
} // namespace esphome
-11
View File
@@ -1,11 +0,0 @@
#include "esphome/core/controller_registry.h"
#ifdef USE_CONTROLLER_REGISTRY
namespace esphome {
StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controllers;
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY
+5 -54
View File
@@ -5,37 +5,18 @@
#ifdef USE_CONTROLLER_REGISTRY
#include "esphome/core/entity_includes.h"
#include "esphome/core/helpers.h"
namespace esphome {
class Controller;
/** Global registry for Controllers to receive entity state updates.
/** Fan-out of entity state updates to the controllers (APIServer, WebServer).
*
* This singleton registry allows Controllers (APIServer, WebServer) to receive
* entity state change notifications without storing per-entity callbacks.
*
* Instead of each entity maintaining controller callbacks (32 bytes overhead per entity),
* entities call ControllerRegistry::notify_*_update() which iterates the small list
* of registered controllers (typically 2: API and WebServer).
*
* Each notify method directly iterates controllers and calls the virtual method,
* avoiding function pointer indirection for minimal dispatch overhead.
*
* Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead)
* Typical config (25 entities): ~780 bytes saved
* Large config (80 entities): ~2,540 bytes saved
* Entities call ControllerRegistry::notify_*_update() instead of holding
* per-entity controller callbacks. The functions are only declared here;
* controller_dispatch.h, included by the generated main.cpp, defines them as
* direct calls on the controllers registered through CORE.register_controller().
*/
class ControllerRegistry {
public:
/** Register a controller to receive entity state updates.
*
* Controllers should call this in their setup() method.
* Typically only APIServer and WebServer register.
*/
static void register_controller(Controller *controller) { controllers.push_back(controller); }
// Notify method declarations (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
@@ -45,38 +26,8 @@ class ControllerRegistry {
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
protected:
static StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> controllers;
};
} // namespace esphome
// Include controller.h AFTER the class definition so notify methods can be
// defined inline. This is safe because controller_registry.h is only ever
// included from .cpp files, never from other headers.
#include "esphome/core/controller.h"
namespace esphome {
// Inline notify methods — each is a tiny loop over 1-2 controllers.
// Defining them here (rather than in controller_registry.cpp) allows the
// compiler to inline them into the single call site in each entity's
// notify_frontend_(), eliminating an unnecessary function-call frame.
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
inline void ControllerRegistry::notify_##callback(type *obj) { \
for (auto *controller : controllers) { \
controller->on_##callback(obj); \
} \
}
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY
-1
View File
@@ -629,7 +629,6 @@
#define USE_DASHBOARD_IMPORT
// Default counts for static analysis
#define CONTROLLER_REGISTRY_MAX 2
#define ESPHOME_AREA_COUNT 10
#define ESPHOME_COMPONENT_COUNT 50
#define ESPHOME_DEVICE_COUNT 10
+26
View File
@@ -2066,6 +2066,32 @@ class LwIPLock {
#endif
};
#if defined(USE_ESP8266) && F_CPU != 160000000L
// Forward decl from <user_interface.h>
// NOLINTNEXTLINE(readability-redundant-declaration)
extern "C" bool system_update_cpu_freq(uint8_t freq);
#endif
/** Runs the CPU at 160 MHz while alive. ESP8266 built for 80 MHz only; elsewhere it compiles to nothing.
*
* The core resets the clock before every loop() pass, so a scope must stay within one pass, must not nest and
* must not yield to the main loop. Peripheral clocks are unchanged, but the cycle counter runs twice as fast, so
* code that times itself against F_CPU, including ISRs that fire while a scope is open, must read CPU2X.
*/
class CpuFrequencyBoost {
public:
CpuFrequencyBoost(const CpuFrequencyBoost &) = delete;
CpuFrequencyBoost &operator=(const CpuFrequencyBoost &) = delete;
#if defined(USE_ESP8266) && F_CPU != 160000000L
CpuFrequencyBoost() { system_update_cpu_freq(160); }
~CpuFrequencyBoost() { system_update_cpu_freq(80); }
#else
// Not = default, so clang-tidy does not flag unused variables at call sites
CpuFrequencyBoost() {}
~CpuFrequencyBoost() {}
#endif
};
/** Helper class to request `loop()` to be called as fast as possible.
*
* Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher
+17 -14
View File
@@ -14,11 +14,8 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size
}
if (this->stack_buffer_ != nullptr && (stack_size > this->stack_size_ || use_psram != this->use_psram_)) {
// Existing buffer is too small or wrong memory type; deallocate to reallocate below
RAMAllocator<StackType_t> allocator(this->use_psram_ ? RAMAllocator<StackType_t>::ALLOC_EXTERNAL
: RAMAllocator<StackType_t>::ALLOC_INTERNAL);
allocator.deallocate(this->stack_buffer_, this->stack_size_);
this->stack_buffer_ = nullptr;
// Existing buffer is too small or wrong memory type; free it to reallocate below
this->stack_buffer_.reset();
}
if (this->stack_buffer_ == nullptr) {
@@ -26,13 +23,23 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size
this->use_psram_ = use_psram;
RAMAllocator<StackType_t> allocator(use_psram ? RAMAllocator<StackType_t>::ALLOC_EXTERNAL
: RAMAllocator<StackType_t>::ALLOC_INTERNAL);
this->stack_buffer_ = allocator.allocate(stack_size);
this->stack_buffer_ = allocator.make_unique_array_for_overwrite(stack_size);
}
if (this->stack_buffer_ == nullptr) {
return false;
}
this->handle_ = xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_, &this->tcb_);
if (this->tcb_ == nullptr) {
RAMAllocator<StaticTask_t> allocator(RAMAllocator<StaticTask_t>::ALLOC_INTERNAL);
this->tcb_ = allocator.make_unique();
}
if (this->tcb_ == nullptr) {
this->deallocate();
return false;
}
this->handle_ =
xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_.get(), this->tcb_.get());
if (this->handle_ == nullptr) {
this->deallocate();
return false;
@@ -65,13 +72,9 @@ bool StaticTask::deallocate() {
if (!this->destroy()) {
return false;
}
if (this->stack_buffer_ != nullptr) {
RAMAllocator<StackType_t> allocator(this->use_psram_ ? RAMAllocator<StackType_t>::ALLOC_EXTERNAL
: RAMAllocator<StackType_t>::ALLOC_INTERNAL);
allocator.deallocate(this->stack_buffer_, this->stack_size_);
this->stack_buffer_ = nullptr;
this->stack_size_ = 0;
}
this->stack_buffer_.reset();
this->stack_size_ = 0;
this->tcb_.reset();
return true;
}
+17 -8
View File
@@ -5,49 +5,58 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "esphome/core/helpers.h"
#include <cstdint>
namespace esphome {
/** Helper for FreeRTOS static task management.
* Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods.
* Stores the TCB in internal RAM and manages the stack buffer with create/destroy methods.
* Call destroy() and deallocate() from another task: a task cannot free the stack it is still running on.
*/
class StaticTask {
public:
StaticTask() = default;
// Non-copyable, non-movable — the task handle and its buffers are not transferable
StaticTask(const StaticTask &) = delete;
StaticTask &operator=(const StaticTask &) = delete;
StaticTask(StaticTask &&) = delete;
StaticTask &operator=(StaticTask &&) = delete;
/// @brief Check if the task has been created and not yet destroyed.
bool is_created() const { return this->handle_ != nullptr; }
/// @brief Get the FreeRTOS task handle.
TaskHandle_t get_handle() const { return this->handle_; }
/// @brief Allocate stack and create task.
/// @brief Allocate the TCB (always internal RAM) and the stack, then create the task.
/// @param fn Task function
/// @param name Task name (for debug)
/// @param stack_size Stack size in bytes (StackType_t is a byte on ESP-IDF)
/// @param param Parameter passed to task function
/// @param priority FreeRTOS task priority
/// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM
/// @return true on success
/// @return true on success; false if either allocation fails or the task cannot be created
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority,
bool use_psram);
/// @brief Delete the task, keeping the stack buffer allocated for reuse by a subsequent create() call.
/// @brief Delete the task, keeping the TCB and stack buffer allocated for reuse by a subsequent create() call.
/// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is
/// suspended here so that it cannot be scheduled again, and it is given no chance to clean up.
/// @return true if the task was deleted; false if it is still running on another core, in which case the
/// caller should try again later.
bool destroy();
/// @brief Delete the task (if created) and free the stack buffer.
/// @return true if the stack buffer was freed; false if the task is still running on another core, in
/// @brief Delete the task (if created) and free the TCB and stack buffer.
/// @return true if the buffers were freed; false if the task is still running on another core, in
/// which case the caller should try again later.
bool deallocate();
protected:
TaskHandle_t handle_{nullptr};
StaticTask_t tcb_;
StackType_t *stack_buffer_{nullptr};
RAMUniquePtr<StaticTask_t> tcb_;
RAMUniquePtr<StackType_t[]> stack_buffer_;
uint32_t stack_size_{0};
bool use_psram_{false};
};
-1
View File
@@ -37,7 +37,6 @@ optional = global_ns.namespace("std").class_("optional")
arduino_json_ns = global_ns.namespace("ArduinoJson")
JsonObject = arduino_json_ns.class_("JsonObject")
JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")
Controller = esphome_ns.class_("Controller")
GPIOPin = esphome_ns.class_("GPIOPin")
InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin)
gpio_ns = esphome_ns.namespace("gpio")
+41 -6
View File
@@ -211,6 +211,27 @@ class OTAEncryptionFallback(OTAError):
"""The encrypted attempt failed and the caller may retry in plaintext."""
# Uploader side option under `ota: encryption:`; the ota component imports the
# name so the upload path never loads the component module
CONF_ALLOW_PLAINTEXT_UPLOAD = "allow_plaintext_upload"
ALLOW_PLAINTEXT_UPLOAD_NOTICE = (
f"'{CONF_ALLOW_PLAINTEXT_UPLOAD}' is set; expected once, on the install that "
"migrates a device which never encrypted. If this device encrypted before, "
"something on the network stripped the offer: remove the option and check "
"the network."
)
# Logged only once the device is seen encrypting, so the migration install
# itself is never nagged and the user learns exactly when removal is safe
ALLOW_PLAINTEXT_UPLOAD_REMOVE_WARNING = f"""
******************************************************************
* This device offers OTA encryption and accepted the key, so
* '{CONF_ALLOW_PLAINTEXT_UPLOAD}' under 'ota: encryption:' has done
* its job. Remove it from the configuration now, together with
* any 'password:' on that block: leaving the option in place lets
* an attacker on the network strip the encryption offer and
* downgrade a future upload to plaintext.
******************************************************************"""
# Remove before 2027.3.0
PLAINTEXT_FALLBACK_NOTICE = (
"A device with an api encryption key offers encryption after this "
@@ -512,6 +533,7 @@ def perform_ota(
ota_type: int = OTA_TYPE_UPDATE_APP,
noise_psk: str | None = None,
plaintext_fallback: bool = False,
allow_plaintext_upload: bool = False,
) -> None:
# Validate up front; an out-of-range value would only surface as a
# ValueError deep inside send_check, bypassing OTAError handling
@@ -577,25 +599,32 @@ def perform_ota(
features = 0
if noise_psk and not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE):
if plaintext_fallback:
# Remove before 2027.3.0: older firmware that cannot encrypt still
# gets its update on this connection
# Remove before 2027.3.0: drop `or plaintext_fallback` and
# PLAINTEXT_FALLBACK_NOTICE here; allow_plaintext_upload stays
if allow_plaintext_upload or plaintext_fallback:
# The running firmware cannot encrypt; it still gets this update,
# and the build being sent offers encryption for the next one
_LOGGER.warning(
"The device did not offer OTA encryption; continuing in plaintext. %s",
PLAINTEXT_FALLBACK_NOTICE,
ALLOW_PLAINTEXT_UPLOAD_NOTICE
if allow_plaintext_upload
else PLAINTEXT_FALLBACK_NOTICE,
)
noise_psk = None
else:
# Fail closed: an attacker could otherwise strip the offer and
# capture the image (wifi credentials, api key)
# Remove before 2027.3.0: installing without the block no longer
# falls back then; advise 'allow_plaintext_upload: true' instead
raise OTAError(
"An OTA encryption key is configured but the device did not "
"offer encryption; refusing to send the image in plaintext. "
"The running firmware predates ESPHome 2026.9.0 or has no "
"'api: encryption: key'. With an api key, install once "
"without the 'ota: encryption:' block (that build offers "
"encryption), then restore it; otherwise flash by serial or "
"the web_server OTA platform."
f"encryption), then restore it; otherwise set '{CONF_ALLOW_PLAINTEXT_UPLOAD}: "
"true' under 'ota: encryption:' for this one install, or flash by "
"serial or the web_server OTA platform."
)
if noise_psk:
# The prologue binds every negotiation byte both sides saw, so any
@@ -619,6 +648,8 @@ def perform_ota(
raise OTAEncryptionFallback(str(err)) from err
raise
_LOGGER.info("Encrypted connection established")
if allow_plaintext_upload:
_LOGGER.warning(ALLOW_PLAINTEXT_UPLOAD_REMOVE_WARNING)
if ota_type != OTA_TYPE_UPDATE_APP:
# Any non-app OTA type requires the extended protocol and the
@@ -824,6 +855,7 @@ def run_ota_impl_(
ota_type: int = OTA_TYPE_UPDATE_APP,
noise_psk: str | None = None,
plaintext_fallback: bool = False,
allow_plaintext_upload: bool = False,
) -> tuple[int, str | None]:
from esphome.core import CORE
@@ -899,6 +931,7 @@ def run_ota_impl_(
ota_type,
encryption.noise_psk,
encryption.plaintext_fallback,
allow_plaintext_upload=allow_plaintext_upload,
)
except OTAEncryptionFallback as err:
# Same address and attempt budget: not a network retry
@@ -940,6 +973,7 @@ def run_ota(
ota_type: int = OTA_TYPE_UPDATE_APP,
noise_psk: str | None = None,
plaintext_fallback: bool = False,
allow_plaintext_upload: bool = False,
) -> tuple[int, str | None]:
try:
return run_ota_impl_(
@@ -950,6 +984,7 @@ def run_ota(
ota_type,
noise_psk,
plaintext_fallback,
allow_plaintext_upload,
)
except OTAError as err:
_LOGGER.error(err)
+11 -12
View File
@@ -212,6 +212,16 @@ VERSION_H_TARGET = "esphome/core/version.h"
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp"
ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h"
# Headers that must not be included bare from esphome.h or the clang-tidy
# all-headers file: X-macro files, headers main.cpp includes itself, and
# deprecated headers that only resolve when their new component is loaded.
ESPHOME_H_EXCLUDE = {
Path(ENTITY_TYPES_H_TARGET),
# main.cpp includes it after defining esphome_controllers()
Path("esphome/core/controller_dispatch.h"),
# moved to components/ring_buffer/, removed in 2026.11.0
Path("esphome/core/ring_buffer.h"),
}
ESPHOME_README_TXT = """
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
@@ -237,20 +247,9 @@ def copy_src_tree():
source_files_l.sort()
# Build #include list for esphome.h
# X-macro files are included multiple times with different macro definitions
# and must not be included bare in esphome.h
# Deprecated headers that re-export from a relocated component must not be
# auto-included, since their #include of the new path only resolves when the
# new component is loaded by a consumer.
esphome_h_exclude = {
Path(ENTITY_TYPES_H_TARGET),
Path(
"esphome/core/ring_buffer.h"
), # moved to components/ring_buffer/, removed in 2026.11.0
}
include_l = []
for target, _ in source_files_l:
if target.suffix in HEADER_FILE_EXTENSIONS and target not in esphome_h_exclude:
if target.suffix in HEADER_FILE_EXTENSIONS and target not in ESPHOME_H_EXCLUDE:
include_l.append(f'#include "{target}"')
include_l.append("")
include_s = "\n".join(include_l)
+1 -1
View File
@@ -32,7 +32,7 @@ ninja==1.13.2 # native esp8266 arduino toolchain build driver
filelock==4.0.1 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
# esp-idf >= 5.0 requires this
pyparsing >= 3.3.2
pyparsing >= 3.3.3
# For autocompletion
argcomplete>=3.7.2
+39 -3
View File
@@ -164,7 +164,21 @@ def lint_post_check(func):
return func
def lint_re_check(regex, **kwargs):
def _nolint_in_match(content, haystack, match, mask):
"""With masking, only a trailing comment counts: the raw span still holds string contents, and
the masked text is blank exactly where comments and strings were, so NOLINT must sit after the
last real code character of the span."""
raw = content[match.start() : match.end()]
if not mask:
return "NOLINT" in raw
masked = haystack[match.start() : match.end()].rstrip()
return "NOLINT" in raw[len(masked) :]
def lint_re_check(regex, mask=False, prefilter=None, **kwargs):
"""mask=True blanks comments and string literals first so prose about the pattern is not reported;
the masked text keeps its length, so match offsets still index the original content.
prefilter is a literal every match must contain, checked before the costlier masking."""
flags = kwargs.pop("flags", re.MULTILINE)
prog = re.compile(regex, flags)
decor = lint_content_check(**kwargs)
@@ -173,8 +187,11 @@ def lint_re_check(regex, **kwargs):
@functools.wraps(func)
def new_func(fname, content):
errs = []
for match in prog.finditer(content):
if "NOLINT" in match.group(0):
if prefilter is not None and prefilter not in content:
return errs
haystack = _mask_cpp_comments_strings(content) if mask else content
for match in prog.finditer(haystack):
if _nolint_in_match(content, haystack, match, mask):
continue
lineno = content.count("\n", 0, match.start()) + 1
substr = content[: match.start()]
@@ -1199,6 +1216,25 @@ def lint_no_std_bind(fname, match):
)
@lint_re_check(
r"[^\w]std\s*::\s*nothrow\b" + CPP_RE_EOL,
mask=True,
prefilter="nothrow",
include=cpp_include,
)
def lint_no_std_nothrow(fname, match):
return (
f"{highlight('new (std::nothrow)')} aborts on ESP-IDF when the allocation fails, exceptions are disabled "
f"there, so it never returns nullptr.\n"
f"Please use {highlight('RAMAllocator')} from esphome/core/helpers.h, which does.\n"
f" Before: {highlight('auto *buf = new (std::nothrow) uint8_t[n];')}\n"
f" After: {highlight('auto buf = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(n);')}\n"
f"For one object use {highlight('RAMAllocator<T>().make_unique(args...)')}; both return empty on failure.\n"
f"Default flags prefer PSRAM; pass RAMAllocator<T>::PREFER_INTERNAL to keep it where new put it.\n"
f"(If strictly necessary, add `// NOLINT` to the end of the line)"
)
LOG_CALL_START_RE = re.compile(r"ESP_LOG\w+\s*\(")
# Comments, raw/plain string literals and single char literals are consumed whole so ; ( ) ? :
# inside them are never seen. A char literal is exactly one (escaped) char so a digit separator
+2 -4
View File
@@ -429,11 +429,9 @@ def build_all_include(header_files: list[str] | None = None) -> None:
if line
]
from esphome.writer import ENTITY_TYPES_H_TARGET
from esphome.writer import ESPHOME_H_EXCLUDE
# X-macro files are included multiple times with different macro definitions
# and must not be included bare in the all-include header
exclude = {ENTITY_TYPES_H_TARGET}
exclude = {path.as_posix() for path in ESPHOME_H_EXCLUDE}
headers = [f'#include "{h}"' for h in header_files if h not in exclude]
headers.sort()
headers.append("")
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
nvs_cache_in_psram: false
psram:
mode: octal
ignore_not_found: false
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
nvs_encryption:
key_id: 0
psram:
mode: octal
ignore_not_found: false
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
sdkconfig_options:
CONFIG_NVS_ENCRYPTION: "y"
psram:
mode: octal
ignore_not_found: false
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
nvs_cache_in_psram: true
psram:
mode: octal
ignore_not_found: false
@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
psram:
mode: octal
ignore_not_found: false
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
psram:
mode: octal
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
sdkconfig_options:
CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM: "n"
psram:
mode: octal
ignore_not_found: false
+135
View File
@@ -4,6 +4,7 @@ Test ESP32 configuration
import asyncio
from collections.abc import Callable
import logging
from pathlib import Path
from typing import Any
@@ -1575,3 +1576,137 @@ def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
defines = {define.name for define in CORE.defines}
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
@pytest.mark.parametrize(
("fixture", "expected"),
[
("nvs_cache_psram_guaranteed.yaml", True),
("nvs_cache_psram_explicit.yaml", True),
("nvs_cache_psram_not_guaranteed.yaml", None),
("nvs_cache_psram_disabled.yaml", None),
# the encryption keys must stay in internal RAM, whichever way encryption is enabled
("nvs_cache_psram_encrypted.yaml", None),
("nvs_cache_psram_encrypted_sdkconfig.yaml", None),
],
)
def test_nvs_cache_in_psram_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
fixture: str,
expected: bool | None,
) -> None:
"""The NVS cache moves to PSRAM only with guaranteed PSRAM, the option not off and no NVS encryption."""
generate_main(component_config_path(fixture))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM") is expected
def test_nvs_cache_in_psram_user_sdkconfig_wins(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A raw sdkconfig_options value for the NVS cache option is left alone."""
generate_main(component_config_path("nvs_cache_psram_user_off.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig["CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM"] == RawSdkconfigValue("n")
@pytest.mark.parametrize(
("full_config", "error_match"),
[
pytest.param(
{CONF_ESPHOME: {}, "psram": {"disabled": False, "ignore_not_found": True}},
r"'nvs_cache_in_psram' requires PSRAM with 'ignore_not_found: false'",
id="nvs_cache_in_psram_needs_guaranteed_psram",
),
pytest.param(
{CONF_ESPHOME: {}},
r"'nvs_cache_in_psram' requires PSRAM with 'ignore_not_found: false'",
id="nvs_cache_in_psram_needs_psram",
),
],
)
def test_nvs_cache_in_psram_explicit_true_errors(
full_config: dict, error_match: str, set_core_config: SetCoreConfigCallable
) -> None:
"""An explicit nvs_cache_in_psram: true that cannot apply is a config error, not a silent no-op."""
set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config)
from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
config = {
"variant": "esp32s3",
"framework": {"type": "esp-idf", "advanced": {"nvs_cache_in_psram": True}},
}
with pytest.raises(cv.Invalid, match=error_match):
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
def test_nvs_cache_in_psram_explicit_true_rejects_encryption(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
full_config={
CONF_ESPHOME: {},
"psram": {"disabled": False, "ignore_not_found": False},
},
)
from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
config = {
"variant": "esp32s3",
"framework": {
"type": "esp-idf",
"advanced": {"nvs_cache_in_psram": True, "nvs_encryption": {"key_id": 0}},
},
}
with pytest.raises(cv.Invalid, match="cannot be used with NVS encryption"):
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
def test_nvs_cache_in_psram_default_with_encryption_is_quiet(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Encryption on a board that never mentioned the option must not warn about it."""
with caplog.at_level(logging.WARNING):
generate_main(component_config_path("nvs_cache_psram_encrypted.yaml"))
assert "nvs_cache_in_psram" not in caplog.text
@pytest.mark.asyncio
async def test_nvs_cache_in_psram_explicit_request_warns_when_encrypted(
set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture
) -> None:
"""An explicit request dropped for NVS encryption enabled elsewhere logs a warning."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_SDKCONFIG_OPTIONS: {}}
)
from esphome.components.esp32 import (
_apply_nvs_cache_in_psram,
add_idf_sdkconfig_option,
)
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
with caplog.at_level(logging.WARNING):
await _apply_nvs_cache_in_psram(True)
assert "nvs_cache_in_psram ignored" in caplog.text
assert (
"CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM"
not in CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
)
def test_nvs_cache_in_psram_explicit_true_on_valid_board_is_quiet(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
"""An explicit true that applies sets the option and warns about nothing."""
with caplog.at_level(logging.WARNING):
generate_main(component_config_path("nvs_cache_psram_explicit.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM") is True
assert "nvs_cache_in_psram" not in caplog.text
+35 -1
View File
@@ -12,10 +12,11 @@ from esphome import config_validation as cv
from esphome.components.esphome.ota import (
AUTO_LOAD,
FILTER_SOURCE_FILES,
_encryption_schema,
_validate_no_password_with_encryption,
ota_esphome_final_validate,
)
from esphome.components.noise import static_encryption_key
from esphome.components.noise import encryption_schema, static_encryption_key
from esphome.const import (
CONF_API,
CONF_ENCRYPTION,
@@ -29,6 +30,7 @@ from esphome.const import (
CONF_VERSION,
)
from esphome.core import CORE, ID
from esphome.espota2 import CONF_ALLOW_PLAINTEXT_UPLOAD
import esphome.final_validate as fv
@@ -198,6 +200,21 @@ def test_encryption_without_any_key_rejected() -> None:
fv.full_config.reset(token)
def test_encryption_schema_allow_plaintext_upload() -> None:
"""The opt in is an ota only option with no default, so a merged block
that does not mention it cannot clear it; the shared api schema does not
know it."""
assert _encryption_schema(None) == {}
assert _encryption_schema({CONF_ALLOW_PLAINTEXT_UPLOAD: True}) == {
CONF_ALLOW_PLAINTEXT_UPLOAD: True
}
assert _encryption_schema({CONF_KEY: API_KEY}) == {CONF_KEY: API_KEY}
with pytest.raises(cv.Invalid):
_encryption_schema(False)
with pytest.raises(cv.Invalid):
encryption_schema({CONF_ALLOW_PLAINTEXT_UPLOAD: True})
def test_encryption_key_mismatch_between_merged_configs_rejected() -> None:
"""Same-port configs with different encryption keys raise."""
full_conf = {
@@ -489,6 +506,13 @@ def test_static_encryption_key() -> None:
{"USE_OTA_ENCRYPTION", "USE_OTA_PASSWORD"},
{"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"},
),
# The migration install keeps the password for the old firmware's
# prompt but the build it sends is authenticated by the key alone
(
"migration_password",
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"},
{"USE_OTA_PASSWORD", "USE_OTA_ENCRYPTION_PROVISIONED"},
),
# The ota encryption block is what makes the device refuse plaintext
(
"encryption_required",
@@ -549,6 +573,16 @@ def test_password_with_encryption_rejected() -> None:
_validate_no_password_with_encryption(config)
def test_password_with_migration_install_accepted() -> None:
"""The old firmware may still ask for the password on the plaintext
leg of the migration install."""
config = {
CONF_PASSWORD: "pw",
CONF_ENCRYPTION: {CONF_KEY: API_KEY, CONF_ALLOW_PLAINTEXT_UPLOAD: True},
}
assert _validate_no_password_with_encryption(config) is config
def test_password_alone_accepted() -> None:
"""A password without encryption still validates."""
config = {CONF_PASSWORD: "pw"}
@@ -0,0 +1,14 @@
esphome:
name: ota-migration-password
host:
api:
encryption:
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
ota:
- platform: esphome
password: "superlongpasswordthatnoonewillknow"
encryption:
allow_plaintext_upload: true
-2
View File
@@ -1,5 +1,4 @@
import esphome.codegen as cg
from esphome.core import CORE
from tests.testing_helpers import ComponentManifestOverride
@@ -12,6 +11,5 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
cg.add_define("API_MAX_SEND_QUEUE", 8)
cg.add_define("MAX_API_CONNECTIONS", 1)
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
CORE.register_controller() # api_server registers with the controller registry
manifest.to_code = to_code_testing
+6
View File
@@ -4,3 +4,9 @@ button:
id: some_button
on_press:
- logger.log: Button pressed
esphome:
on_boot:
- button.press: some_button
- button.press:
id: some_button
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration:
default: 10s
gpio_wakeup_reason: 30s
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration:
default: 10s
gpio_wakeup_reason: 30s
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration:
default: 10s
gpio_wakeup_reason: 30s
+4
View File
@@ -4,6 +4,10 @@ esphome:
- deep_sleep.prevent
- delay: 1s
- deep_sleep.allow
- deep_sleep.prevent:
id: deep_sleep_1
- deep_sleep.allow:
id: deep_sleep_1
- if:
condition:
lambda: 'return false;'
@@ -6,6 +6,7 @@ packages:
deep_sleep: !include common.yaml
deep_sleep:
id: deep_sleep_1
run_duration: 10s
zephyr_ble_server:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration: 30s
sleep_duration: 12h
on_wake:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration: 10s
sleep_duration: 50s
on_wake:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration: 10s
sleep_duration: 50s
+3
View File
@@ -104,6 +104,9 @@ button:
fan.is_on: test_fan
then:
- fan.turn_off: test_fan
- fan.toggle: test_fan
- fan.turn_off:
id: test_fan
- if:
condition:
fan.is_off:
+2 -1
View File
@@ -3,7 +3,8 @@ esphome:
then:
- lock.lock: test_lock1
- lock.unlock: test_lock1
- lock.open: test_lock1
- lock.open:
id: test_lock1
- if:
condition:
lock.is_locked: test_lock1
@@ -0,0 +1,12 @@
# capture, stop_capture and is_capturing on the external microphone.
button:
- platform: template
name: Microphone Capture
on_press:
- if:
condition:
microphone.is_capturing: mic_id_external
then:
- microphone.stop_capture: mic_id_external
else:
- microphone.capture: mic_id_external
@@ -0,0 +1,8 @@
substitutions:
i2s_din_pin1: GPIO33
i2s_din_pin2: GPIO34
packages:
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml
microphone: !include common-pdm.yaml
actions: !include common-actions.yaml
+7
View File
@@ -101,6 +101,13 @@ button:
payload: Hello
qos: 2
retain: true
- mqtt.publish:
topic: !lambda return "some/topic/button/lambda";
payload: !lambda |-
std::string payload = "Hello";
return payload;
qos: !lambda return 1;
retain: !lambda return false;
climate:
- platform: thermostat

Some files were not shown because too many files have changed in this diff Show More