mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Merge branch 'pch-strict-ci' into host-pch
# Conflicts: # esphome/writer.py
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
Application,
|
||||
Component,
|
||||
ComponentPtr,
|
||||
Controller,
|
||||
EntityBase,
|
||||
EntityCategory,
|
||||
ESPTime,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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,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 = (
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+13
-13
@@ -212,6 +212,18 @@ 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"),
|
||||
# build machinery, not user API
|
||||
Path("esphome/core/pch_prefix.h"),
|
||||
}
|
||||
ESPHOME_README_TXT = """
|
||||
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
|
||||
|
||||
@@ -237,21 +249,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
|
||||
Path("esphome/core/pch_prefix.h"), # build machinery, not user API
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user