Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch

This commit is contained in:
J. Nick Koston
2026-09-24 14:10:04 +01:00
85 changed files with 759 additions and 1165 deletions
+13
View File
@@ -470,6 +470,19 @@ file does, and it is the authority when they disagree. The most useful starting
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
**Conditions that only test their parent need no C++ class either.** Register them with
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares
against config values.
```python
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
automation.register_apply_condition(
"my_component.state_is",
schema,
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
)
```
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
* **Configuration Validation:**
@@ -4,7 +4,6 @@
# Home Assistant Add-on: ESPHome
# Sends discovery information to Home Assistant.
# ==============================================================================
declare config
declare port
# We only disable it when disabled explicitly
@@ -19,14 +18,17 @@ port=$(bashio::addon.ingress_port)
# Wait for the ESPHome Device Builder to become available
bashio::net.wait_for "${port}" "127.0.0.1" 300
config=$(\
bashio::var.json \
host "127.0.0.1" \
port "^${port}" \
)
# Send one discovery message; the config is a JSON string built with bashio::var.json.
send_discovery() {
local service=$1
local config=$2
if bashio::discovery "${service}" "${config}" > /dev/null; then
bashio::log.info "Successfully send ${service} discovery information to Home Assistant."
else
bashio::log.error "${service} discovery message to Home Assistant failed!"
fi
}
if bashio::discovery "esphome" "${config}" > /dev/null; then
bashio::log.info "Successfully send discovery information to Home Assistant."
else
bashio::log.error "Discovery message to Home Assistant failed!"
fi
send_discovery "esphome" "$(bashio::var.json host "127.0.0.1" port "^${port}")"
# The Device Builder MCP server, consumed by Home Assistant's mcp integration.
send_discovery "mcp" "$(bashio::var.json url "http://127.0.0.1:${port}/api/mcp")"
+127 -42
View File
@@ -214,6 +214,7 @@ validate_condition = cv.validate_registry_entry("condition", CONDITION_REGISTRY)
validate_condition_list = cv.validate_registry("condition", CONDITION_REGISTRY)
ApplyAction = cg.esphome_ns.class_("ApplyAction", Action)
ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition)
def flash_string(config: ConfigType, value: str) -> str:
@@ -223,6 +224,15 @@ def flash_string(config: ConfigType, value: str) -> str:
return str(cg.safe_exp(value))
def literal_with_length(config: ConfigType, value: str) -> str:
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
The target compares or copies the bytes in place, so it needs the RAM literal rather than
the PROGMEM rendering on ESP8266, and the length saves a strlen.
"""
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
@dataclass(frozen=True)
class ApplyCall:
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
@@ -255,6 +265,13 @@ class ApplyCall:
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
)
@property
def members(self) -> list[tuple[Any, Any, Any]]:
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
return [
(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in self.args
]
@dataclass(frozen=True)
class ApplyField:
@@ -322,6 +339,58 @@ def _check_key_in_schema(
schema = schema.schema[markers[part]]
async def _apply_parent(config: ConfigType) -> str:
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
return f"::{await cg.get_variable(config[CONF_ID])}"
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
# pointer conversion.
return [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
for t, arg in args
]
async def _render_values(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str,
lambda_args: TemplateArgsType,
compare: bool = False,
) -> list[str]:
"""Render the argument text of one statement; every key must be present.
``compare``: an inlined lambda expression is parenthesized so it binds as a whole
beside an operator.
"""
if any(value is None for value in values):
keys = [key for key, _, _ in members]
raise EsphomeError(f"{name}: {target!r} needs all of {keys}")
exprs: list[str] = []
for (_, type_, const_fn), value in zip(members, values, strict=True):
if isinstance(value, Lambda):
if isinstance(type_, str):
type_ = cg.RawExpression(type_.format(parent=parent))
inner = await cg.process_lambda(value, lambda_args, return_type=type_)
expr = call_lambda(inner)
bare = compare and isinstance(expr, cg.RawExpression)
exprs.append(f"({expr})" if bare else str(expr))
elif const_fn is not None:
exprs.append(const_fn(config, value))
else:
exprs.append(str(cg.safe_exp(value)))
return exprs
def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> list[Any]:
return [_config_lookup(config, key) for key, _, _ in members]
def register_apply_action(
name: str,
schema: cv.Schema,
@@ -334,10 +403,14 @@ def register_apply_action(
in, lambdas are called inline with the trigger args. With ``call`` every statement targets
the call object ``auto apply_call = parent->call()``, and ``apply_call.perform()`` is appended.
"""
# An action stores the value, so a std::string constant stays in flash on ESP8266.
statements_spec = [
(
c.target,
[(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in c.args],
[
(key, t, fn or (flash_string if t is cg.std_string else None))
for key, t, fn in c.members
],
)
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
]
@@ -351,37 +424,17 @@ def register_apply_action(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
parent = f"::{await cg.get_variable(config[CONF_ID])}"
# Must match ApplyAction::ApplyFn exactly for the function pointer conversion.
lambda_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
for t, arg in args
]
parent = await _apply_parent(config)
lambda_args = _apply_lambda_args(args)
receiver = "apply_call." if call else f"{parent}->"
statements: list[str] = []
for target, members in statements_spec:
values = [_config_lookup(config, key) for key, _, _ in members]
values = _apply_values(config, members)
if members and all(value is None for value in values):
continue
if any(value is None for value in values):
keys = [key for key, _, _ in members]
raise EsphomeError(f"{name}: {target!r} needs all of {keys}")
exprs: list[str] = []
for (_, type_, const_fn), value in zip(members, values, strict=True):
if isinstance(value, Lambda):
if isinstance(type_, str):
type_ = cg.RawExpression(type_.format(parent=parent))
inner = await cg.process_lambda(
value, lambda_args, return_type=type_
)
exprs.append(str(call_lambda(inner)))
elif const_fn is not None:
exprs.append(const_fn(config, value))
elif type_ is cg.std_string:
exprs.append(flash_string(config, value))
else:
exprs.append(str(cg.safe_exp(value)))
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args
)
statements.append(f"{receiver}{target.format(*exprs)};")
if call:
statements = [
@@ -397,6 +450,51 @@ def register_apply_action(
register_action(name, ApplyAction, schema, synchronous=True)(builder)
def register_apply_condition(
name: str, schema: cv.Schema, check: str | ApplyCall
) -> None:
"""Register a condition that is one expression on its parent, with no C++ class.
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
against config values, all of which must be present. Write ``== false`` to negate.
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
Generates one stateless function for ``ApplyCondition<Ts...>``.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
members = call.members
for conf_key, _, _ in members:
_check_key_in_schema(name, schema, conf_key)
async def builder(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await _apply_parent(config)
lambda_args = _apply_lambda_args(args)
exprs = await _render_values(
name,
call.target,
members,
_apply_values(config, members),
config,
parent,
lambda_args,
compare=True,
)
check_lambda = LambdaExpression(
[f"return {parent}->{call.target.format(*exprs)};"],
lambda_args,
capture="",
return_type=cg.bool_,
)
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
register_condition(name, ApplyCondition, schema)(builder)
def validate_potentially_and_condition(value):
if isinstance(value, list):
with cv.remove_prepend_path(["and"]):
@@ -643,28 +741,15 @@ async def for_condition_to_code(
return var
@register_condition(
register_apply_condition(
"component.is_idle",
LambdaCondition,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.Component),
}
),
"is_idle()",
)
async def component_is_idle_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
lambda_ = await cg.process_lambda(
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_action(
@@ -5,16 +5,13 @@
#include "esphome/components/noise/noise.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include <cstring>
#include <cinttypes>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome::api {
using noise::noise_err_to_logstr;
@@ -26,11 +23,7 @@ static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
"api and noise component handshake size limits must match");
static const char *const TAG = "api.noise";
#ifdef USE_ESP8266
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
#else
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
@@ -72,11 +65,7 @@ APIError APINoiseFrameHelper::init() {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
#ifdef USE_ESP8266
memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#else
std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#endif
progmem_memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
state_ = State::CLIENT_HELLO;
return APIError::OK;
+11 -38
View File
@@ -2,9 +2,8 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_VOLUME
from esphome.core import ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.const import CONF_VOLUME
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
@@ -13,10 +12,6 @@ IS_PLATFORM_COMPONENT = True
audio_dac_ns = cg.esphome_ns.namespace("audio_dac")
AudioDac = audio_dac_ns.class_("AudioDac")
MuteOffAction = audio_dac_ns.class_("MuteOffAction", automation.Action)
MuteOnAction = audio_dac_ns.class_("MuteOnAction", automation.Action)
SetVolumeAction = audio_dac_ns.class_("SetVolumeAction", automation.Action)
MUTE_ACTION_SCHEMA = maybe_simple_id(
{
@@ -33,41 +28,19 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
)
@automation.register_action(
"audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
)
async def audio_dac_mute_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 (
("audio_dac.mute_off", "set_mute_off()"),
("audio_dac.mute_on", "set_mute_on()"),
):
automation.register_apply_action(
_name, MUTE_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@automation.register_action(
automation.register_apply_action(
"audio_dac.set_volume",
SetVolumeAction,
SET_VOLUME_ACTION_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_VOLUME, "set_volume", cg.float_),
)
async def audio_dac_set_volume_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config.get(CONF_VOLUME), args, cg.float_)
cg.add(var.set_volume(template_))
return var
@coroutine_with_priority(CoroPriority.CORE)
-41
View File
@@ -1,41 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "audio_dac.h"
namespace esphome::audio_dac {
template<typename... Ts> class MuteOffAction final : public Action<Ts...> {
public:
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); }
protected:
AudioDac *audio_dac_;
};
template<typename... Ts> class MuteOnAction final : public Action<Ts...> {
public:
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); }
protected:
AudioDac *audio_dac_;
};
template<typename... Ts> class SetVolumeAction final : public Action<Ts...> {
public:
explicit SetVolumeAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
TEMPLATABLE_VALUE(float, volume)
void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
protected:
AudioDac *audio_dac_;
};
} // namespace esphome::audio_dac
+5 -16
View File
@@ -1,7 +1,7 @@
from logging import getLogger
from esphome import automation, core
from esphome.automation import Condition, maybe_simple_id
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
from esphome.components.const import CONF_ON_STATE_CHANGE
@@ -138,9 +138,6 @@ BinarySensorInvalidateAction = binary_sensor_ns.class_(
"BinarySensorInvalidateAction", automation.Action
)
# Condition
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
# Filters
Filter = binary_sensor_ns.class_("Filter")
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter)
@@ -645,20 +642,12 @@ BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
)
@automation.register_condition(
"binary_sensor.is_on", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
automation.register_apply_condition(
"binary_sensor.is_on", BINARY_SENSOR_CONDITION_SCHEMA, "state"
)
async def binary_sensor_is_on_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, True)
@automation.register_condition(
"binary_sensor.is_off", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
automation.register_apply_condition(
"binary_sensor.is_off", BINARY_SENSOR_CONDITION_SCHEMA, "state == false"
)
async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, False)
@coroutine_with_priority(CoroPriority.CORE)
@@ -155,16 +155,6 @@ class StateChangeTrigger final : public Trigger<optional<bool>, optional<bool> >
}
};
template<typename... Ts> class BinarySensorCondition final : public Condition<Ts...> {
public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
protected:
BinarySensor *parent_;
bool state_;
};
template<typename... Ts> class BinarySensorInvalidateAction final : public Action<Ts...> {
public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
+2 -8
View File
@@ -494,12 +494,6 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
)
def _literal_with_length(config: ConfigType, value: str) -> str:
# The (const char *, size_t) overload compares bytes in place, so it needs a plain
# literal rather than the default PROGMEM rendering on ESP8266, and skips a strlen.
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
automation.register_apply_action(
"climate.control",
CLIMATE_CONTROL_ACTION_SCHEMA,
@@ -517,14 +511,14 @@ automation.register_apply_action(
CONF_CUSTOM_FAN_MODE,
"set_fan_mode",
cg.std_string,
const_fn=_literal_with_length,
const_fn=automation.literal_with_length,
),
automation.ApplyField(CONF_PRESET, "set_preset", ClimatePreset),
automation.ApplyField(
CONF_CUSTOM_PRESET,
"set_preset",
cg.std_string,
const_fn=_literal_with_length,
const_fn=automation.literal_with_length,
),
automation.ApplyField(CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode),
call="make_call",
+5 -13
View File
@@ -13,17 +13,7 @@ climate::ClimateTraits ClimateIR::traits() {
if (this->humidity_sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (this->supports_heat_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
if (this->supports_dry_)
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->supports_fan_only_)
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
traits.set_supported_modes(this->modes_);
traits.set_visual_min_temperature(this->minimum_temperature_);
traits.set_visual_max_temperature(this->maximum_temperature_);
@@ -98,8 +88,10 @@ void ClimateIR::dump_config() {
" Supports HEAT: %s\n"
" Supports COOL: %s\n"
" Supports HEAT_COOL: %s",
this->minimum_temperature_, this->maximum_temperature_, YESNO(this->supports_heat_),
YESNO(this->supports_cool_), YESNO(this->supports_heat_cool_));
this->minimum_temperature_, this->maximum_temperature_,
YESNO(this->modes_.count(climate::CLIMATE_MODE_HEAT)),
YESNO(this->modes_.count(climate::CLIMATE_MODE_COOL)),
YESNO(this->modes_.count(climate::CLIMATE_MODE_HEAT_COOL)));
}
} // namespace esphome::climate_ir
+21 -11
View File
@@ -30,8 +30,10 @@ class ClimateIR : public Component,
this->minimum_temperature_ = minimum_temperature;
this->maximum_temperature_ = maximum_temperature;
this->temperature_step_ = temperature_step;
this->supports_dry_ = supports_dry;
this->supports_fan_only_ = supports_fan_only;
if (supports_dry)
this->modes_.insert(climate::CLIMATE_MODE_DRY);
if (supports_fan_only)
this->modes_.insert(climate::CLIMATE_MODE_FAN_ONLY);
this->fan_modes_ = fan_modes;
this->swing_modes_ = swing_modes;
this->presets_ = presets;
@@ -39,9 +41,11 @@ class ClimateIR : public Component,
void setup() override;
void dump_config() override;
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; }
void set_supports_cool(bool supports_cool) { this->set_mode_supported_(climate::CLIMATE_MODE_COOL, supports_cool); }
void set_supports_heat(bool supports_heat) { this->set_mode_supported_(climate::CLIMATE_MODE_HEAT, supports_heat); }
void set_supports_heat_cool(bool supports_heat_cool) {
this->set_mode_supported_(climate::CLIMATE_MODE_HEAT_COOL, supports_heat_cool);
}
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
@@ -59,12 +63,18 @@ class ClimateIR : public Component,
// Dummy implement on_receive so implementation is optional for inheritors
bool on_receive(remote_base::RemoteReceiveData data) override { return false; };
bool supports_cool_{true};
bool supports_heat_{true};
// Default (supports_cool && supports_heat) is resolved during code generation.
bool supports_heat_cool_{true};
bool supports_dry_{false};
bool supports_fan_only_{false};
ESPHOME_ALWAYS_INLINE void set_mode_supported_(climate::ClimateMode mode, bool supported) {
if (supported) {
this->modes_.insert(mode);
} else {
this->modes_.erase(mode);
}
}
// The HEAT_COOL default (supports_cool && supports_heat) is resolved during code generation.
static constexpr climate::ClimateModeMask DEFAULT_MODES{climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_HEAT_COOL};
climate::ClimateModeMask modes_{DEFAULT_MODES};
climate::ClimateFanModeMask fan_modes_{};
climate::ClimateSwingModeMask swing_modes_{};
climate::ClimatePresetMask presets_{};
+9 -18
View File
@@ -1,7 +1,7 @@
import logging
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
@@ -36,7 +36,7 @@ from esphome.const import (
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
)
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,
@@ -44,7 +44,7 @@ from esphome.core.entity_helpers import (
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
from esphome.types import ConfigType, SafeExpType
IS_PLATFORM_COMPONENT = True
@@ -87,8 +87,6 @@ COVER_OPERATIONS = {
validate_cover_operation = cv.enum(COVER_OPERATIONS, upper=True)
# Actions
CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition)
CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition)
CoverOpenedTrigger = cover_ns.class_(
"CoverOpenedTrigger", automation.Trigger.template()
)
@@ -287,19 +285,12 @@ COVER_CONDITION_SCHEMA = cv.maybe_simple_value(
)
async def cover_condition_to_code(
config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
automation.register_condition(
"cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA
)(cover_condition_to_code)
automation.register_condition(
"cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA
)(cover_condition_to_code)
automation.register_apply_condition(
"cover.is_open", COVER_CONDITION_SCHEMA, f"position == {COVER_OPEN}"
)
automation.register_apply_condition(
"cover.is_closed", COVER_CONDITION_SCHEMA, f"position == {COVER_CLOSED}"
)
@coroutine_with_priority(CoroPriority.CORE)
-13
View File
@@ -6,19 +6,6 @@
namespace esphome::cover {
template<bool OPEN, typename... Ts> class CoverPositionCondition final : public Condition<Ts...> {
public:
CoverPositionCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); }
protected:
Cover *cover_;
};
template<typename... Ts> using CoverIsOpenCondition = CoverPositionCondition<true, Ts...>;
template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<false, Ts...>;
template<bool OPEN> class CoverPositionTrigger final : public Trigger<> {
public:
CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) {
+40 -245
View File
@@ -37,23 +37,6 @@ DEVICE = {
"TF_CARD": Device.TF_CARD,
}
NextAction = dfplayer_ns.class_("NextAction", automation.Action)
PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action)
PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action)
PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action)
PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action)
SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action)
VolumeUpAction = dfplayer_ns.class_("VolumeUpAction", automation.Action)
VolumeDownAction = dfplayer_ns.class_("VolumeDownAction", automation.Action)
SetEqAction = dfplayer_ns.class_("SetEqAction", automation.Action)
SleepAction = dfplayer_ns.class_("SleepAction", automation.Action)
ResetAction = dfplayer_ns.class_("ResetAction", automation.Action)
StartAction = dfplayer_ns.class_("StartAction", automation.Action)
PauseAction = dfplayer_ns.class_("PauseAction", automation.Action)
StopAction = dfplayer_ns.class_("StopAction", automation.Action)
RandomAction = dfplayer_ns.class_("RandomAction", automation.Action)
SetDeviceAction = dfplayer_ns.class_("SetDeviceAction", automation.Action)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -87,41 +70,30 @@ async def to_code(config):
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(
"dfplayer.play_next",
NextAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
DFPLAYER_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
)
async def dfplayer_next_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
for _name, _call in (
("dfplayer.play_next", "next()"),
("dfplayer.play_previous", "previous()"),
("dfplayer.volume_up", "volume_up()"),
("dfplayer.volume_down", "volume_down()"),
("dfplayer.sleep", "sleep()"),
("dfplayer.reset", "reset()"),
("dfplayer.start", "start()"),
("dfplayer.pause", "pause()"),
("dfplayer.stop", "stop()"),
("dfplayer.random", "random()"),
):
automation.register_apply_action(
_name, DFPLAYER_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@automation.register_action(
"dfplayer.play_previous",
PreviousAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_previous_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(
automation.register_apply_action(
"dfplayer.play_mp3",
PlayMp3Action,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
@@ -129,70 +101,43 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args):
},
key=CONF_FILE,
),
synchronous=True,
automation.ApplyField(CONF_FILE, "play_mp3", cg.uint16),
)
async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
cg.add(var.set_file(template_))
return var
@automation.register_action(
# loop and file default to what the old action's unset templatable values evaluated to
automation.register_apply_action(
"dfplayer.play",
PlayFileAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
cv.Required(CONF_FILE): cv.templatable(cv.int_),
cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
cv.Optional(CONF_LOOP, default=False): cv.templatable(cv.boolean),
},
key=CONF_FILE,
),
synchronous=True,
automation.ApplyCall(
"play_file({}, {})", ((CONF_FILE, cg.uint16), (CONF_LOOP, cg.bool_))
),
)
async def dfplayer_play_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
cg.add(var.set_file(template_))
if CONF_LOOP in config:
template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_)
cg.add(var.set_loop(template_))
return var
@automation.register_action(
automation.register_apply_action(
"dfplayer.play_folder",
PlayFolderAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
cv.Required(CONF_FOLDER): cv.templatable(cv.int_),
cv.Optional(CONF_FILE): cv.templatable(cv.int_),
cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
cv.Optional(CONF_FILE, default=0): cv.templatable(cv.int_),
cv.Optional(CONF_LOOP, default=False): cv.templatable(cv.boolean),
}
),
synchronous=True,
automation.ApplyCall(
"play_folder({}, {}, {})",
((CONF_FOLDER, cg.uint16), (CONF_FILE, cg.uint16), (CONF_LOOP, cg.bool_)),
),
)
async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FOLDER], args, cg.uint16)
cg.add(var.set_folder(template_))
if CONF_FILE in config:
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
cg.add(var.set_file(template_))
if CONF_LOOP in config:
template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_)
cg.add(var.set_loop(template_))
return var
@automation.register_action(
automation.register_apply_action(
"dfplayer.set_device",
SetDeviceAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
@@ -200,19 +145,11 @@ async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
},
key=CONF_DEVICE,
),
synchronous=True,
automation.ApplyField(CONF_DEVICE, "set_device", Device),
)
async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_DEVICE], args, Device)
cg.add(var.set_device(template_))
return var
@automation.register_action(
automation.register_apply_action(
"dfplayer.set_volume",
SetVolumeAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
@@ -220,51 +157,11 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
},
key=CONF_VOLUME,
),
synchronous=True,
automation.ApplyField(CONF_VOLUME, "set_volume", cg.uint8),
)
async def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_VOLUME], args, cg.uint8)
cg.add(var.set_volume(template_))
return var
@automation.register_action(
"dfplayer.volume_up",
VolumeUpAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_volume_up_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(
"dfplayer.volume_down",
VolumeDownAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_volume_down_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(
automation.register_apply_action(
"dfplayer.set_eq",
SetEqAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
@@ -272,110 +169,8 @@ async def dfplayer_volume_down_to_code(config, action_id, template_arg, args):
},
key=CONF_EQ_PRESET,
),
synchronous=True,
automation.ApplyField(CONF_EQ_PRESET, "set_eq", EqPreset),
)
async def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_EQ_PRESET], args, EqPreset)
cg.add(var.set_eq(template_))
return var
@automation.register_action(
"dfplayer.sleep",
SleepAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_sleep_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(
"dfplayer.reset",
ResetAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_reset_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(
"dfplayer.start",
StartAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_start_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(
"dfplayer.pause",
PauseAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_pause_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(
"dfplayer.stop",
StopAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_stop_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(
"dfplayer.random",
RandomAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_random_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
def _default_enable(value: Any) -> Any:
+5 -94
View File
@@ -33,8 +33,13 @@ class DFPlayer final : public uart::UARTDevice, public Component {
void play_mp3(uint16_t file);
void play_file(uint16_t file);
void play_file_loop(uint16_t file);
void play_file(uint16_t file, bool loop) { loop ? this->play_file_loop(file) : this->play_file(file); }
void play_folder(uint16_t folder, uint16_t file);
void play_folder_loop(uint16_t folder);
// The loop command plays the whole folder, so file is ignored when loop is set.
void play_folder(uint16_t folder, uint16_t file, bool loop) {
loop ? this->play_folder_loop(folder) : this->play_folder(folder, file);
}
void volume_up();
void volume_down();
void set_device(Device device);
@@ -72,100 +77,6 @@ class DFPlayer final : public uart::UARTDevice, public Component {
CallbackManager<void()> on_finished_playback_callback_;
};
#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \
template<typename... Ts> \
class ACTION_CLASS : /* NOLINT */ \
public Action<Ts...>, \
public Parented<DFPlayer> { \
void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \
};
DFPLAYER_SIMPLE_ACTION(NextAction, next)
DFPLAYER_SIMPLE_ACTION(PreviousAction, previous)
template<typename... Ts> class PlayMp3Action final : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(uint16_t, file)
void play(const Ts &...x) override {
auto file = this->file_.value(x...);
this->parent_->play_mp3(file);
}
};
template<typename... Ts> class PlayFileAction final : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop)
void play(const Ts &...x) override {
auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...);
if (loop) {
this->parent_->play_file_loop(file);
} else {
this->parent_->play_file(file);
}
}
};
template<typename... Ts> class PlayFolderAction final : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(uint16_t, folder)
TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop)
void play(const Ts &...x) override {
auto folder = this->folder_.value(x...);
auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...);
if (loop) {
this->parent_->play_folder_loop(folder);
} else {
this->parent_->play_folder(folder, file);
}
}
};
template<typename... Ts> class SetDeviceAction final : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(Device, device)
void play(const Ts &...x) override {
auto device = this->device_.value(x...);
this->parent_->set_device(device);
}
};
template<typename... Ts> class SetVolumeAction final : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(uint8_t, volume)
void play(const Ts &...x) override {
auto volume = this->volume_.value(x...);
this->parent_->set_volume(volume);
}
};
template<typename... Ts> class SetEqAction final : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(EqPreset, eq)
void play(const Ts &...x) override {
auto eq = this->eq_.value(x...);
this->parent_->set_eq(eq);
}
};
DFPLAYER_SIMPLE_ACTION(SleepAction, sleep)
DFPLAYER_SIMPLE_ACTION(ResetAction, reset)
DFPLAYER_SIMPLE_ACTION(StartAction, start)
DFPLAYER_SIMPLE_ACTION(PauseAction, pause)
DFPLAYER_SIMPLE_ACTION(StopAction, stop)
DFPLAYER_SIMPLE_ACTION(RandomAction, random)
DFPLAYER_SIMPLE_ACTION(VolumeUpAction, volume_up)
DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down)
template<typename... Ts> class DFPlayerIsPlayingCondition final : public Condition<Ts...>, public Parented<DFPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
+8 -1
View File
@@ -133,10 +133,17 @@ TRIGGER_EVENT_SCHEMA = cv.Schema(
)
def _event_type_literal(config: ConfigType, value: str) -> str:
"""A constant event type is a plain literal; trigger() only compares it, so no copy is needed."""
return str(cg.safe_exp(value))
automation.register_apply_action(
"event.trigger",
TRIGGER_EVENT_SCHEMA,
automation.ApplyField(CONF_EVENT_TYPE, "trigger", cg.std_string),
automation.ApplyField(
CONF_EVENT_TYPE, "trigger", cg.std_string, _event_type_literal
),
)
+3 -11
View File
@@ -7,17 +7,17 @@ namespace esphome::event {
static const char *const TAG = "event";
void Event::trigger(const std::string &event_type) {
void Event::trigger(const char *event_type) {
// Linear search with strcmp - faster than std::set for small datasets (1-5 items typical)
const char *found = nullptr;
for (const char *type : this->types_) {
if (strcmp(type, event_type.c_str()) == 0) {
if (strcmp(type, event_type) == 0) {
found = type;
break;
}
}
if (found == nullptr) {
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type);
return;
}
this->last_event_type_ = found;
@@ -36,12 +36,4 @@ void Event::set_event_types(const FixedVector<const char *> &event_types) {
this->last_event_type_ = nullptr; // Reset when types change
}
void Event::set_event_types(const std::vector<const char *> &event_types) {
this->types_.init(event_types.size());
for (const char *type : event_types) {
this->types_.push_back(type);
}
this->last_event_type_ = nullptr; // Reset when types change
}
} // namespace esphome::event
+5 -7
View File
@@ -3,7 +3,6 @@
#include <cstring>
#include <limits>
#include <string>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
@@ -21,22 +20,21 @@ namespace esphome::event {
class Event : public EntityBase {
public:
void trigger(const std::string &event_type);
/// Trigger an event; the type is matched against the configured types by string compare.
void trigger(const char *event_type);
void trigger(const std::string &event_type) { this->trigger(event_type.c_str()); }
/// Set the event types supported by this event (from initializer list).
/// Set the event types supported by this event; called by generated code with string literals.
void set_event_types(std::initializer_list<const char *> event_types) {
this->types_ = event_types;
this->last_event_type_ = nullptr; // Reset when types change
}
/// Set the event types supported by this event (from FixedVector).
/// Copy the event types of another event, for components that wrap one.
void set_event_types(const FixedVector<const char *> &event_types);
/// Set the event types supported by this event (from vector).
void set_event_types(const std::vector<const char *> &event_types);
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_event_types(std::initializer_list<std::string> event_types) = delete;
void set_event_types(const FixedVector<std::string> &event_types) = delete;
void set_event_types(const std::vector<std::string> &event_types) = delete;
/// Return the event types supported by this event.
const FixedVector<const char *> &get_event_types() const { return this->types_; }
+8 -22
View File
@@ -83,9 +83,6 @@ FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
)
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
_FAN_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
@@ -367,27 +364,16 @@ async def fan_cycle_speed_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"fan.is_on",
FanIsOnCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Fan),
}
),
FAN_CONDITION_SCHEMA = automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Fan),
}
)
@automation.register_condition(
"fan.is_off",
FanIsOffCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Fan),
}
),
automation.register_apply_condition("fan.is_on", FAN_CONDITION_SCHEMA, "state")
automation.register_apply_condition(
"fan.is_off", FAN_CONDITION_SCHEMA, "state == false"
)
async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
@coroutine_with_priority(CoroPriority.CORE)
-17
View File
@@ -68,23 +68,6 @@ template<typename... Ts> class CycleSpeedAction final : public Action<Ts...> {
Fan *state_;
};
template<typename... Ts> class FanIsOnCondition final : public Condition<Ts...> {
public:
explicit FanIsOnCondition(Fan *state) : state_(state) {}
bool check(const Ts &...x) override { return this->state_->state; }
protected:
Fan *state_;
};
template<typename... Ts> class FanIsOffCondition final : public Condition<Ts...> {
public:
explicit FanIsOffCondition(Fan *state) : state_(state) {}
bool check(const Ts &...x) override { return !this->state_->state; }
protected:
Fan *state_;
};
class FanStateTrigger final : public Trigger<Fan *> {
public:
FanStateTrigger(Fan *state) : fan_(state) {
-17
View File
@@ -117,23 +117,6 @@ template<bool Forward, typename... Ts> class LightEffectCycleAction final : publ
bool include_none_{false};
};
template<typename... Ts> class LightIsOnCondition final : public Condition<Ts...> {
public:
explicit LightIsOnCondition(LightState *state) : state_(state) {}
bool check(const Ts &...x) override { return this->state_->current_values.is_on(); }
protected:
LightState *state_;
};
template<typename... Ts> class LightIsOffCondition final : public Condition<Ts...> {
public:
explicit LightIsOffCondition(LightState *state) : state_(state) {}
bool check(const Ts &...x) override { return !this->state_->current_values.is_on(); }
protected:
LightState *state_;
};
class LightTurnOnTrigger final : public Trigger<>, public LightRemoteValuesListener {
public:
explicit LightTurnOnTrigger(LightState *a_light) : light_(a_light) {
+10 -21
View File
@@ -38,8 +38,6 @@ from .types import (
ColorMode,
DimRelativeAction,
LightEffectCycleAction,
LightIsOffCondition,
LightIsOnCondition,
LightState,
ToggleAction,
)
@@ -381,24 +379,15 @@ async def light_addressable_set_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"light.is_on",
LightIsOnCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
}
),
LIGHT_CONDITION_SCHEMA = automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
}
)
@automation.register_condition(
"light.is_off",
LightIsOffCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
}
),
automation.register_apply_condition(
"light.is_on", LIGHT_CONDITION_SCHEMA, "current_values.is_on()"
)
automation.register_apply_condition(
"light.is_off", LIGHT_CONDITION_SCHEMA, "current_values.is_on() == false"
)
async def light_is_on_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
-2
View File
@@ -44,8 +44,6 @@ ToggleAction = light_ns.class_("ToggleAction", automation.Action)
LightEffectCycleAction = light_ns.class_("LightEffectCycleAction", automation.Action)
DimRelativeAction = light_ns.class_("DimRelativeAction", automation.Action)
AddressableSet = light_ns.class_("AddressableSet", automation.Action)
LightIsOnCondition = light_ns.class_("LightIsOnCondition", automation.Condition)
LightIsOffCondition = light_ns.class_("LightIsOffCondition", automation.Condition)
# Triggers
LightTurnOnTrigger = light_ns.class_(
+9 -22
View File
@@ -1,5 +1,5 @@
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
@@ -34,7 +34,6 @@ LockAction = lock_ns.class_("LockAction", automation.Action)
OpenAction = lock_ns.class_("OpenAction", automation.Action)
LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action)
LockCondition = lock_ns.class_("LockCondition", Condition)
LockStateForwarder = lock_ns.class_("LockStateForwarder")
LockState = lock_ns.enum("LockState")
@@ -154,26 +153,14 @@ async def lock_action_to_code(
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA)
async def lock_is_on_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, True)
@automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA)
async def lock_is_off_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, False)
automation.register_apply_condition(
"lock.is_locked", LOCK_ACTION_SCHEMA, f"state == {LockState.LOCK_STATE_LOCKED}"
)
automation.register_apply_condition(
"lock.is_unlocked",
LOCK_ACTION_SCHEMA,
f"state == {LockState.LOCK_STATE_UNLOCKED}",
)
@coroutine_with_priority(CoroPriority.CORE)
-13
View File
@@ -36,19 +36,6 @@ template<typename... Ts> class OpenAction final : public Action<Ts...> {
Lock *lock_;
};
template<typename... Ts> class LockCondition final : public Condition<Ts...> {
public:
LockCondition(Lock *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override {
auto check_state = this->state_ ? LockState::LOCK_STATE_LOCKED : LockState::LOCK_STATE_UNLOCKED;
return this->parent_->state == check_state;
}
protected:
Lock *parent_;
bool state_;
};
/// 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 {
-3
View File
@@ -32,9 +32,6 @@ enum LockState : uint8_t {
};
const LogString *lock_state_to_string(LockState state);
/// Maximum length of lock state string (including null terminator): "UNLOCKING" = 10
static constexpr size_t LOCK_STATE_STR_SIZE = 10;
class LockTraits {
public:
LockTraits() = default;
+17 -4
View File
@@ -1,3 +1,4 @@
import logging
import re
from typing import Any
@@ -63,6 +64,8 @@ from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
logger_ns = cg.esphome_ns.namespace("logger")
LOG_LEVELS = {
@@ -105,6 +108,7 @@ DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id"
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
CONF_WAIT_FOR_CDC = "wait_for_cdc"
@@ -219,6 +223,18 @@ def validate_initial_no_higher_than_global(config: ConfigType) -> ConfigType:
return config
def warn_ram_log_strings(config: ConfigType) -> ConfigType:
# Remove before 2027.4.0
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH) is False:
_LOGGER.warning(
"'%s: false' is ignored and will be rejected in 2027.4.0. Log format strings "
"always stay in flash now; copying them into RAM gave no speed gain and the "
"lost RAM caused crashes. Remove the option",
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH,
)
return config
def validate_wait_for_cdc(config: ConfigType) -> ConfigType:
if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC:
raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC")
@@ -232,7 +248,6 @@ LoggerMessageTrigger = logger_ns.class_(
)
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -332,6 +347,7 @@ CONFIG_SCHEMA = cv.All(
validate_local_no_higher_than_global,
validate_initial_no_higher_than_global,
validate_wait_for_cdc,
warn_ram_log_strings,
)
@@ -450,9 +466,6 @@ async def _late_logger_init(config: ConfigType) -> None:
cg.add_build_flag("-DCORE_DEBUG_LEVEL=5")
if CORE.is_esp32 and is_at_least_very_verbose:
cg.add_build_flag("-DENABLE_I2C_DEBUG_BUFFER")
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
if CORE.is_esp32:
if config[CONF_HARDWARE_UART] == USB_CDC:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
+5 -5
View File
@@ -105,9 +105,9 @@ struct LogBuffer {
this->format_vsnprintf_(format, args);
this->finalize_();
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void HOT format_body_P(PGM_P format, va_list args) {
this->format_vsnprintf_P_(format, args);
#ifdef USE_ESP8266
void HOT format_body_p(PGM_P format, va_list args) {
this->format_vsnprintf_p_(format, args);
this->finalize_();
}
#endif
@@ -158,8 +158,8 @@ struct LogBuffer {
return;
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void format_vsnprintf_P_(PGM_P format, va_list args) {
#ifdef USE_ESP8266
void format_vsnprintf_p_(PGM_P format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
+3 -4
View File
@@ -127,9 +127,8 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
}
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Implementation for ESP8266 with flash string support.
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
#ifdef USE_ESP8266
// ESP8266 keeps log format strings in flash.
//
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
// Uses vsnprintf_P to read the format string directly from flash without copying to RAM.
@@ -141,7 +140,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
}
#endif // USE_STORE_LOG_STR_IN_FLASH
#endif // USE_ESP8266
inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
+8 -8
View File
@@ -200,9 +200,9 @@ class Logger final : public Component {
float get_setup_priority() const override { return setup_priority::BUS + 500.0f; }
void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT
#ifdef USE_STORE_LOG_STR_IN_FLASH
void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
va_list args); // NOLINT
#ifdef USE_ESP8266
// NOLINTNEXTLINE(readability-identifier-naming)
void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args);
#endif
protected:
@@ -244,14 +244,14 @@ class Logger final : public Component {
buf.format_body(format, args);
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
// ESP8266-only (single-task), thread_name is always nullptr
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
inline void HOT format_log_to_buffer_with_terminator_p_(uint8_t level, const char *tag, int line,
const __FlashStringHelper *format, va_list args,
LogBuffer &buf) {
buf.write_header(level, tag, line, nullptr);
buf.format_body_P(reinterpret_cast<PGM_P>(format), args);
buf.format_body_p(reinterpret_cast<PGM_P>(format), args);
}
#endif
@@ -283,9 +283,9 @@ class Logger final : public Component {
FormatType format, va_list args, const char *thread_name) {
RecursionGuard guard(recursion_guard);
LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE};
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
if constexpr (std::is_same_v<FormatType, const __FlashStringHelper *>) {
this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf);
this->format_log_to_buffer_with_terminator_p_(level, tag, line, format, args, buf);
} else
#endif
{
+2 -2
View File
@@ -292,8 +292,8 @@ BASE_PROPS = {
"LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH"
).several_of,
"text_font": lv_font,
"text_letter_space": lvalid.lv_positive_int,
"text_line_space": lvalid.lv_positive_int,
"text_letter_space": lvalid.lv_int,
"text_line_space": lvalid.lv_int,
"text_opa": lvalid.opacity,
"text_outline_stroke_color": lvalid.lv_color,
"text_outline_stroke_opa": lvalid.opacity,
+2 -6
View File
@@ -5,6 +5,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
// On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by
// IP state listener events on whichever network interface is configured.
#if (defined(USE_ESP8266) || defined(USE_RP2)) && \
@@ -39,12 +40,7 @@ struct MDNSString;
// Macro to cast string literals to MDNSString* (works on all platforms)
#define MDNS_STR(name) (reinterpret_cast<const esphome::mdns::MDNSString *>(name))
#ifdef USE_ESP8266
#include <pgmspace.h>
#define MDNS_STR_ARG(s) ((PGM_P) (s))
#else
#define MDNS_STR_ARG(s) (reinterpret_cast<const char *>(s))
#endif
#define MDNS_STR_ARG(s) (reinterpret_cast<ESPHOME_PGM_P>(s))
// Service count is calculated at compile time by Python codegen
// MDNS_SERVICE_COUNT will always be defined
+18 -30
View File
@@ -190,16 +190,15 @@ _STATE_TRIGGERS = (
(CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF),
)
# State conditions that all share the same schema and codegen handler
_STATE_CONDITIONS = [
"idle",
"paused",
"playing",
"announcing",
"on",
"off",
"muted",
]
# State conditions: (config_key suffix, checked state)
_STATE_CONDITIONS = (
("idle", MediaPlayerState.MEDIA_PLAYER_STATE_IDLE),
("paused", MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED),
("playing", MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING),
("announcing", MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING),
("on", MediaPlayerState.MEDIA_PLAYER_STATE_ON),
("off", MediaPlayerState.MEDIA_PLAYER_STATE_OFF),
)
MediaPlayerCommand = media_player_ns.enum("MediaPlayerCommand", is_class=True)
@@ -360,27 +359,16 @@ for _action_name in _COMMAND_ACTIONS:
)
def _snake_to_camel(name):
return "".join(word.capitalize() for word in name.split("_"))
for _condition_name, _state in _STATE_CONDITIONS:
automation.register_apply_condition(
f"media_player.is_{_condition_name}",
MEDIA_PLAYER_CONDITION_SCHEMA,
f"state == {_state}",
)
def _register_state_conditions():
async def handler(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
for condition_name in _STATE_CONDITIONS:
class_name = f"Is{_snake_to_camel(condition_name)}Condition"
condition_class = media_player_ns.class_(class_name, automation.Condition)
automation.register_condition(
f"media_player.is_{condition_name}",
condition_class,
MEDIA_PLAYER_CONDITION_SCHEMA,
)(handler)
_register_state_conditions()
automation.register_apply_condition(
"media_player.is_muted", MEDIA_PLAYER_CONDITION_SCHEMA, "is_muted()"
)
automation.register_apply_action(
@@ -27,41 +27,4 @@ static_assert(std::is_trivially_copyable_v<StateAnyForwarder>);
static_assert(sizeof(StateEnterForwarder<MediaPlayerState::MEDIA_PLAYER_STATE_IDLE>) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<StateEnterForwarder<MediaPlayerState::MEDIA_PLAYER_STATE_IDLE>>);
template<typename... Ts> class IsIdleCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; }
};
template<typename... Ts> class IsPlayingCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; }
};
template<typename... Ts> class IsPausedCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; }
};
template<typename... Ts> class IsAnnouncingCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override {
return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
}
};
template<typename... Ts> class IsOnCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; }
};
template<typename... Ts> class IsOffCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; }
};
template<typename... Ts> class IsMutedCondition final : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->is_muted(); }
};
} // namespace esphome::media_player
+1 -1
View File
@@ -102,7 +102,7 @@ void MitsubishiClimate::transmit_state() {
default:
remote_state[6] = MITSUBISHI_MODE_COOL;
remote_state[8] = MITSUBISHI_MODE_A_COOL;
if (this->supports_heat_) {
if (this->modes_.count(climate::CLIMATE_MODE_HEAT)) {
remote_state[6] = MITSUBISHI_MODE_HEAT;
remote_state[8] = MITSUBISHI_MODE_A_HEAT;
}
+4 -4
View File
@@ -51,10 +51,10 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
{climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST,
climate::CLIMATE_PRESET_SLEEP}) {}
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; }
void set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_supports_dry(bool supports_dry) { this->set_mode_supported_(climate::CLIMATE_MODE_DRY, supports_dry); }
void set_supports_fan_only(bool supports_fan_only) {
this->set_mode_supported_(climate::CLIMATE_MODE_FAN_ONLY, supports_fan_only);
}
void set_fan_mode(SetFanMode fan_mode) {
this->fan_mode_ = fan_mode;
+2 -7
View File
@@ -1,5 +1,4 @@
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components import logger, socket
from esphome.components.esp32 import (
@@ -135,7 +134,6 @@ MQTTDisconnectTrigger = mqtt_ns.class_(
"MQTTDisconnectTrigger", automation.Trigger.template(MQTTClientDisconnectReason)
)
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
"MQTTAlarmControlPanelComponent", MQTTComponent
@@ -599,18 +597,15 @@ async def register_mqtt_component(var, config):
)
@automation.register_condition(
automation.register_apply_condition(
"mqtt.connected",
MQTTConnectedCondition,
cv.Schema(
{
cv.GenerateID(): cv.use_id(MQTTClientComponent),
}
),
"is_connected()",
)
async def mqtt_connected_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
@automation.register_action(
-9
View File
@@ -417,15 +417,6 @@ template<typename... Ts> class MQTTPublishJsonAction final : public Action<Ts...
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTConnectedCondition final : public Condition<Ts...> {
public:
MQTTConnectedCondition(MQTTClientComponent *parent) : parent_(parent) {}
bool check(const Ts &...x) override { return this->parent_->is_connected(); }
protected:
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTEnableAction final : public Action<Ts...> {
public:
MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {}
+1 -5
View File
@@ -312,11 +312,7 @@ bool MQTTComponent::send_discovery_() {
// Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus
// safety margin
char version_buf[sizeof(ver_fmt) + 8];
#ifdef USE_ESP8266
snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
#else
snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
#endif
ESPHOME_snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
device_info[MQTT_DEVICE_SW_VERSION] = version_buf;
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
#if defined(USE_ESP8266) || defined(USE_ESP32)
+2 -8
View File
@@ -50,14 +50,8 @@ bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }
bool MQTTLockComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_STORE_LOG_STR_IN_FLASH
char buf[LOCK_STATE_STR_SIZE];
strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return this->publish(this->get_state_topic_to_(topic_buf), buf);
#else
return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
#endif
return this->publish(this->get_state_topic_to_(topic_buf),
reinterpret_cast<ProgmemStr>(lock_state_to_string(this->lock_->state)));
}
} // namespace esphome::mqtt
+2 -5
View File
@@ -1,6 +1,7 @@
#include <cinttypes>
#include "mqtt_sensor.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h"
@@ -59,11 +60,7 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
root[MQTT_FORCE_UPDATE] = true;
if (this->sensor_->get_state_class() != STATE_CLASS_NONE) {
#ifdef USE_STORE_LOG_STR_IN_FLASH
root[MQTT_STATE_CLASS] = (const __FlashStringHelper *) state_class_to_string(this->sensor_->get_state_class());
#else
root[MQTT_STATE_CLASS] = LOG_STR_ARG(state_class_to_string(this->sensor_->get_state_class()));
#endif
root[MQTT_STATE_CLASS] = reinterpret_cast<ProgmemStr>(state_class_to_string(this->sensor_->get_state_class()));
}
config.command_topic = false;
+4 -16
View File
@@ -2,16 +2,13 @@
#ifdef USE_NOISE
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cstring>
#include <noise/protocol.h>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome::noise {
static const char *const TAG = "noise";
@@ -74,22 +71,13 @@ size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *rea
return 0;
}
buf[0] = HANDSHAKE_STATUS_REJECT;
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, capacity - 1);
if (reason_len > 0) {
memcpy_P(buf + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// The reason may live in PROGMEM on ESP8266; the progmem helpers read RAM and flash alike
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, capacity - 1);
size_t reason_len = std::min(ESPHOME_strlen_P(reason_str), capacity - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(buf + 1, reason_str, reason_len);
progmem_memcpy(buf + 1, reason_str, reason_len);
}
#endif
return reason_len + 1;
}
-7
View File
@@ -6,13 +6,6 @@
namespace esphome::number {
class NumberStateTrigger final : public Trigger<float> {
public:
explicit NumberStateTrigger(Number *parent) {
parent->add_on_state_callback([this](float value) { this->trigger(value); });
}
};
class ValueRangeTrigger final : public Trigger<float>, public Component {
public:
explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}
+11 -33
View File
@@ -14,8 +14,7 @@ from esphome.const import (
CONF_TYPE,
CONF_URL,
)
from esphome.core import ID, Lambda
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.core import Lambda
from esphome.types import ConfigType
AUTO_LOAD = ["runtime_image"]
@@ -31,14 +30,6 @@ OnlineImage = online_image_ns.class_(
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
)
# Actions
SetUrlAction = online_image_ns.class_(
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
)
ReleaseImageAction = online_image_ns.class_(
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
)
ONLINE_IMAGE_SCHEMA = (
runtime_image.runtime_image_schema(OnlineImage)
.extend(
@@ -90,31 +81,18 @@ RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
)
@automation.register_action(
"online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True
automation.register_apply_action(
"online_image.set_url",
SET_URL_SCHEMA,
automation.ApplyField(CONF_URL, "set_url", cg.std_string),
automation.ApplyField(CONF_UPDATE, "update_if", cg.bool_),
)
@automation.register_action(
"online_image.release",
ReleaseImageAction,
RELEASE_IMAGE_SCHEMA,
synchronous=True,
)
async def online_image_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_URL in config:
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
if CONF_UPDATE in config:
template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_)
cg.add(var.set_update(template_))
return var
automation.register_apply_action(
"online_image.release",
RELEASE_IMAGE_SCHEMA,
automation.ApplyCall("release()"),
)
_CALLBACK_AUTOMATIONS = (
+5 -25
View File
@@ -43,6 +43,11 @@ class OnlineImage final : public PollingComponent,
bool is_big_endian = false);
void update() override;
/** Download now when `update` is true (the `update` flag of `online_image.set_url`). */
void update_if(bool update) {
if (update)
this->update();
}
void loop() override;
/** Set the URL to download the image from. */
@@ -104,29 +109,4 @@ class OnlineImage final : public PollingComponent,
uint32_t start_time_{0};
};
template<typename... Ts> class OnlineImageSetUrlAction final : public Action<Ts...> {
public:
OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(bool, update)
void play(const Ts &...x) override {
this->parent_->set_url(this->url_.value(x...));
if (this->update_.value(x...)) {
this->parent_->update();
}
}
protected:
OnlineImage *parent_;
};
template<typename... Ts> class OnlineImageReleaseAction final : public Action<Ts...> {
public:
OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->release(); }
protected:
OnlineImage *parent_;
};
} // namespace esphome::online_image
@@ -144,14 +144,8 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st
}
}
#ifdef USE_ESP8266
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name,
EntityBase *obj, std::string &area, std::string &node,
std::string &friendly_name) {
#else
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj,
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, ProgmemStr metric_name, EntityBase *obj,
std::string &area, std::string &node, std::string &friendly_name) {
#endif
stream->print(metric_name);
stream->print(ESPHOME_F("{id=\""));
stream->print(relabel_id_(obj).c_str());
@@ -903,11 +897,7 @@ void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *ob
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",operation=\""));
#ifdef USE_STORE_LOG_STR_IN_FLASH
stream->print((const __FlashStringHelper *) valve::valve_operation_to_str(obj->current_operation));
#else
stream->print((const char *) valve::valve_operation_to_str(obj->current_operation));
#endif
stream->print(reinterpret_cast<ProgmemStr>(valve::valve_operation_to_str(obj->current_operation)));
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
@@ -947,7 +937,11 @@ void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climat
stream->print(ESPHOME_F("\",category=\""));
stream->print(setting.c_str());
stream->print(ESPHOME_F("\",setting_value=\""));
#ifdef USE_ESP8266
stream->print((const __FlashStringHelper *) setting_value);
#else
stream->print(LOG_STR_ARG(setting_value));
#endif
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
@@ -8,6 +8,7 @@
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/progmem.h"
#ifdef USE_CLIMATE
#include "esphome/core/log.h"
#endif
@@ -68,13 +69,8 @@ class PrometheusHandler final : public AsyncWebHandler, public Component {
void add_node_label_(AsyncResponseStream *stream, std::string &node);
void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name);
/// Print metric name and common labels (id, area, node, friendly_name, name)
#ifdef USE_ESP8266
void print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, EntityBase *obj,
std::string &area, std::string &node, std::string &friendly_name);
#else
void print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, std::string &area,
void print_metric_labels_(AsyncResponseStream *stream, ProgmemStr metric_name, EntityBase *obj, std::string &area,
std::string &node, std::string &friendly_name);
#endif
#ifdef USE_SENSOR
/// Return the type for prometheus
@@ -4,7 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::rotary_encoder {
@@ -106,15 +105,4 @@ class RotaryEncoderSensor final : public sensor::Sensor, public Component {
CallbackManager<void(int32_t)> listeners_{};
};
template<typename... Ts> class RotaryEncoderSetValueAction final : public Action<Ts...> {
public:
RotaryEncoderSetValueAction(RotaryEncoderSensor *encoder) : encoder_(encoder) {}
TEMPLATABLE_VALUE(int, value)
void play(const Ts &...x) override { this->encoder_->set_value(this->value_.value(x...)); }
protected:
RotaryEncoderSensor *encoder_;
};
} // namespace esphome::rotary_encoder
+2 -19
View File
@@ -15,8 +15,6 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_STEPS,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
rotary_encoder_ns = cg.esphome_ns.namespace("rotary_encoder")
@@ -42,9 +40,6 @@ CONF_PUBLISH_INITIAL_VALUE = "publish_initial_value"
RotaryEncoderSensor = rotary_encoder_ns.class_(
"RotaryEncoderSensor", sensor.Sensor, cg.Component
)
RotaryEncoderSetValueAction = rotary_encoder_ns.class_(
"RotaryEncoderSetValueAction", automation.Action
)
def validate_min_max_value(config: ConfigType) -> ConfigType:
@@ -118,25 +113,13 @@ async def to_code(config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(
automation.register_apply_action(
"sensor.rotary_encoder.set_value",
RotaryEncoderSetValueAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(RotaryEncoderSensor),
cv.Required(CONF_VALUE): cv.templatable(cv.int_),
}
),
synchronous=True,
automation.ApplyField(CONF_VALUE, "set_value", cg.int_),
)
async def sensor_template_publish_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALUE], args, cg.int_)
cg.add(var.set_value(template_))
return var
+2 -3
View File
@@ -18,7 +18,6 @@ CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
rtttl_ns = cg.esphome_ns.namespace("rtttl")
Rtttl = rtttl_ns.class_("Rtttl", cg.Component)
IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition)
MULTI_CONF = True
@@ -120,12 +119,12 @@ automation.register_apply_action(
)
automation.register_parented_condition(
automation.register_apply_condition(
"rtttl.is_playing",
IsPlayingCondition,
cv.Schema(
{
cv.GenerateID(): cv.use_id(Rtttl),
}
),
"is_playing()",
)
-6
View File
@@ -1,6 +1,5 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
@@ -116,9 +115,4 @@ class Rtttl final : public Component {
#endif
};
template<typename... Ts> class IsPlayingCondition final : public Condition<Ts...>, public Parented<Rtttl> {
public:
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
};
} // namespace esphome::rtttl
+2 -7
View File
@@ -11,7 +11,6 @@ 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)
IsRunningCondition = script_ns.class_("IsRunningCondition", automation.Condition)
SingleScript = script_ns.class_("SingleScript", Script)
RestartScript = script_ns.class_("RestartScript", Script)
QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component)
@@ -233,12 +232,8 @@ async def script_wait_action_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
automation.register_apply_condition(
"script.is_running",
IsRunningCondition,
automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
"is_running()",
)
async def script_is_running_to_code(config, condition_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(condition_id, template_arg, paren)
+1 -7
View File
@@ -5,14 +5,8 @@ namespace esphome::script {
static const char *const TAG = "script";
#ifdef USE_STORE_LOG_STR_IN_FLASH
void ScriptLogger::esp_log_(int level, int line, const __FlashStringHelper *format, const char *param) {
void ScriptLogger::esp_log_(int level, int line, ProgmemStr format, const char *param) {
esp_log_printf_(level, TAG, line, format, param);
}
#else
void ScriptLogger::esp_log_(int level, int line, const char *format, const char *param) {
esp_log_printf_(level, TAG, line, format, param);
}
#endif
} // namespace esphome::script
+4 -23
View File
@@ -8,28 +8,19 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::script {
class ScriptLogger {
protected:
#ifdef USE_STORE_LOG_STR_IN_FLASH
void esp_logw_(int line, const __FlashStringHelper *format, const char *param) {
void esp_logw_(int line, ProgmemStr format, const char *param) {
esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param);
}
void esp_logd_(int line, const __FlashStringHelper *format, const char *param) {
void esp_logd_(int line, ProgmemStr format, const char *param) {
esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param);
}
void esp_log_(int level, int line, const __FlashStringHelper *format, const char *param);
#else
void esp_logw_(int line, const char *format, const char *param) {
esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param);
}
void esp_logd_(int line, const char *format, const char *param) {
esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param);
}
void esp_log_(int level, int line, const char *format, const char *param);
#endif
void esp_log_(int level, int line, ProgmemStr format, const char *param);
};
/// The abstract base class for all script types.
@@ -276,16 +267,6 @@ template<class C, typename... Ts> class ScriptStopAction final : public Action<T
C *script_;
};
template<class C, typename... Ts> class IsRunningCondition final : public Condition<Ts...> {
public:
explicit IsRunningCondition(C *parent) : parent_(parent) {}
bool check(const Ts &...x) override { return this->parent_->is_running(); }
protected:
C *parent_;
};
/** Wait for a script to finish before continuing.
*
* Uses queue-based storage to safely handle concurrent executions.
+6 -1
View File
@@ -142,7 +142,12 @@ automation.register_apply_action(
cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
}
),
automation.ApplyField(CONF_OPTION, "set_option", cg.std_string),
automation.ApplyField(
CONF_OPTION,
"set_option",
cg.std_string,
const_fn=automation.literal_with_length,
),
call="make_call",
)
-14
View File
@@ -6,20 +6,6 @@
namespace esphome::sensor {
class SensorStateTrigger final : public Trigger<float> {
public:
explicit SensorStateTrigger(Sensor *parent) {
parent->add_on_state_callback([this](float value) { this->trigger(value); });
}
};
class SensorRawStateTrigger final : public Trigger<float> {
public:
explicit SensorRawStateTrigger(Sensor *parent) {
parent->add_on_raw_state_callback([this](float value) { this->trigger(value); });
}
};
class ValueRangeTrigger final : public Trigger<float>, public Component {
public:
explicit ValueRangeTrigger(Sensor *parent) : parent_(parent) {}
+5 -13
View File
@@ -1,5 +1,5 @@
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
import esphome.config_validation as cv
@@ -54,7 +54,6 @@ RESTORE_MODES = {
}
SwitchCondition = switch_ns.class_("SwitchCondition", Condition)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
@@ -201,17 +200,10 @@ for _name, _call in (
_name, SWITCH_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@automation.register_condition("switch.is_on", SwitchCondition, SWITCH_ACTION_SCHEMA)
async def switch_is_on_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, True)
@automation.register_condition("switch.is_off", SwitchCondition, SWITCH_ACTION_SCHEMA)
async def switch_is_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, False)
automation.register_apply_condition("switch.is_on", SWITCH_ACTION_SCHEMA, "state")
automation.register_apply_condition(
"switch.is_off", SWITCH_ACTION_SCHEMA, "state == false"
)
@coroutine_with_priority(CoroPriority.CORE)
-10
View File
@@ -6,16 +6,6 @@
namespace esphome::switch_ {
template<typename... Ts> class SwitchCondition final : public Condition<Ts...> {
public:
SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
protected:
Switch *parent_;
bool state_;
};
class SwitchStateTrigger final : public Trigger<bool> {
public:
SwitchStateTrigger(Switch *a_switch) {
+2 -12
View File
@@ -43,10 +43,6 @@ text_sensor_ns = cg.esphome_ns.namespace("text_sensor")
TextSensor = text_sensor_ns.class_("TextSensor", cg.EntityBase)
TextSensorPtr = TextSensor.operator("ptr")
TextSensorStateCondition = text_sensor_ns.class_(
"TextSensorStateCondition", automation.Condition
)
FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@@ -240,22 +236,16 @@ async def to_code(config):
cg.add_global(text_sensor_ns.using)
@automation.register_condition(
automation.register_apply_condition(
"text_sensor.state",
TextSensorStateCondition,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(TextSensor),
cv.Required(CONF_STATE): cv.templatable(cv.string_strict),
}
),
automation.ApplyCall("state == {}", ((CONF_STATE, cg.std_string),)),
)
async def text_sensor_state_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(condition_id, template_arg, paren)
templ = await cg.templatable(config[CONF_STATE], args, cg.std_string)
cg.add(var.set_state(templ))
return var
FILTER_SOURCE_FILES = filter_source_files_from_defines(
@@ -1,37 +0,0 @@
#pragma once
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::text_sensor {
class TextSensorStateTrigger final : public Trigger<std::string> {
public:
explicit TextSensorStateTrigger(TextSensor *parent) {
parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); });
}
};
class TextSensorStateRawTrigger final : public Trigger<std::string> {
public:
explicit TextSensorStateRawTrigger(TextSensor *parent) {
parent->add_on_raw_state_callback([this](const std::string &value) { this->trigger(value); });
}
};
template<typename... Ts> class TextSensorStateCondition final : public Condition<Ts...> {
public:
explicit TextSensorStateCondition(TextSensor *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, state)
bool check(const Ts &...x) override { return this->parent_->state == this->state_.value(x...); }
protected:
TextSensor *parent_;
};
} // namespace esphome::text_sensor
+2 -8
View File
@@ -6,7 +6,6 @@ import logging
import tzlocal
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_defines
@@ -18,7 +17,6 @@ from esphome.const import (
CONF_DAYS_OF_WEEK,
CONF_HOUR,
CONF_HOURS,
CONF_ID,
CONF_MINUTE,
CONF_MINUTES,
CONF_MONTHS,
@@ -49,7 +47,6 @@ time_ns = cg.esphome_ns.namespace("time")
RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent)
CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component)
SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component)
TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition)
# C++ types for pre-parsed timezone struct generation
DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True)
@@ -469,18 +466,15 @@ async def to_code(config):
cg.add_global(time_ns.using)
@automation.register_condition(
automation.register_apply_condition(
"time.has_time",
TimeHasTimeCondition,
cv.Schema(
{
cv.GenerateID(): cv.use_id(RealTimeClock),
}
),
"now().is_valid()",
)
async def time_has_time_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
# posix_tz.cpp is fully #ifdef'd on USE_TIME_TIMEZONE, set only when a
@@ -44,13 +44,4 @@ class RealTimeClock : public PollingComponent {
LazyCallbackManager<void()> time_sync_callback_;
};
template<typename... Ts> class TimeHasTimeCondition final : public Condition<Ts...> {
public:
TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {}
bool check(const Ts &...x) override { return this->parent_->now().is_valid(); }
protected:
RealTimeClock *parent_;
};
} // namespace esphome::time
+3 -6
View File
@@ -8,6 +8,7 @@
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/progmem.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
@@ -36,12 +37,8 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
namespace esphome::web_server {
// Type for parameter names that can be stored in flash on ESP8266
#ifdef USE_ESP8266
using ParamNameType = const __FlashStringHelper *;
#else
using ParamNameType = const char *;
#endif
// Parameter names live in flash on ESP8266
using ParamNameType = ProgmemStr;
// All platforms need to defer actions to main loop thread.
// Multi-core platforms need this for thread safety.
+12
View File
@@ -267,6 +267,18 @@ template<typename... Ts> class ApplyAction final : public Action<Ts...> {
ApplyFn apply_;
};
/// Condition counterpart of ApplyAction: one codegen-generated predicate with the parent baked in.
template<typename... Ts> class ApplyCondition final : public Condition<Ts...> {
public:
using CheckFn = bool (*)(const std::remove_cvref_t<Ts> &...);
explicit ApplyCondition(CheckFn check) : check_(check) {}
bool check(const Ts &...x) override { return this->check_(x...); }
protected:
CheckFn check_;
};
/// Simple continuation action that calls play_next_ on a parent action.
/// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions.
/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function).
+1 -1
View File
@@ -36,7 +36,7 @@ void HOT esp_log_printf_(int level, const char *tag, int line, const char *forma
#endif
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
void HOT esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...) {
#ifdef USE_LOGGER
ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr);
+6 -5
View File
@@ -16,7 +16,7 @@
#include <cinttypes>
#include <string>
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
#include "WString.h"
#endif
@@ -63,7 +63,8 @@ namespace esphome {
void esp_log_printf_(int level, const char *tag, int line, const char *format, ...) // NOLINT
__attribute__((format(printf, 4, 5)));
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
// NOLINTNEXTLINE(readability-identifier-naming)
void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...);
#endif
void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT
@@ -71,7 +72,7 @@ void esp_log_vprintf_(int level, const char *tag, int line, const char *format,
int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#endif
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
#define ESPHOME_LOG_FORMAT(format) F(format)
#else
#define ESPHOME_LOG_FORMAT(format) format
@@ -172,7 +173,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
// Helper class that identifies strings that may be stored in flash storage (similar to Arduino's __FlashStringHelper)
struct LogString;
#ifdef USE_STORE_LOG_STR_IN_FLASH
#ifdef USE_ESP8266
#include <pgmspace.h>
@@ -181,7 +182,7 @@ struct LogString;
#define LOG_STR(s) (reinterpret_cast<const LogString *>(PSTR(s)))
#define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s))
#else // !USE_STORE_LOG_STR_IN_FLASH
#else // !USE_ESP8266
#define LOG_STR(s) (reinterpret_cast<const LogString *>(s))
#define LOG_STR_ARG(s) (reinterpret_cast<const char *>(s))
+4 -9
View File
@@ -24,19 +24,13 @@
#define ESPHOME_strcasecmp_P strcasecmp_P
#define ESPHOME_strncmp_P strncmp_P
#define ESPHOME_strncasecmp_P strncasecmp_P
#define ESPHOME_strlen_P strlen_P
// Type for pointers to PROGMEM strings (for use with ESPHOME_F return values)
using ProgmemStr = const __FlashStringHelper *;
// Storage class for PROGMEM_STRING_TABLE data. Mirrors the logger's choice of
// LOG_STR_ARG: when LOG_STR_ARG treats the LogString as PROGMEM (PGM_P), the
// table data must actually be in flash; when LOG_STR_ARG treats it as a plain
// const char* (assumes RAM), the table data must live in RAM or non-logger
// consumers (ArduinoJson, Print, MQTT publish) crash on unaligned flash reads.
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Storage class for PROGMEM_STRING_TABLE data; LOG_STR_ARG treats a LogString as PGM_P
// on ESP8266, so the table data must be in flash to match.
#define ESPHOME_PROGMEM_STRING_TABLE_STORAGE PROGMEM
#else
#define ESPHOME_PROGMEM_STRING_TABLE_STORAGE
#endif
#else
#define ESPHOME_F(string_literal) (string_literal)
#define ESPHOME_PGM_P const char *
#define ESPHOME_PSTR(s) (s)
@@ -47,6 +41,7 @@ using ProgmemStr = const __FlashStringHelper *;
#define ESPHOME_strcasecmp_P strcasecmp
#define ESPHOME_strncmp_P strncmp
#define ESPHOME_strncasecmp_P strncasecmp
#define ESPHOME_strlen_P strlen
// Type for pointers to strings (no PROGMEM on non-ESP8266 platforms)
using ProgmemStr = const char *;
// No-op on non-ESP8266 platforms where PROGMEM itself is a no-op.
@@ -1,7 +1,13 @@
"""Tests for the logger component."""
from collections.abc import Callable
from pathlib import Path
import re
import pytest
from esphome.core import CORE
def test_logger_pre_setup_before_other_components(generate_main):
"""Logger::pre_setup() must be called before any other component is created.
@@ -84,3 +90,24 @@ def test_libretiny_uart0_is_emitted(generate_main):
)
assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp
def test_ram_log_strings_is_deprecated(
generate_main: Callable[[str | Path], str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""`false` is accepted but ignored: no RAM build flag, and a warning with the removal version."""
generate_main("tests/component_tests/logger/test_logger_ram_log_strings.yaml")
assert not any("STORE_LOG_STR" in flag for flag in CORE.build_flags)
assert "esp8266_store_log_strings_in_flash: false' is ignored" in caplog.text
assert "2027.4.0" in caplog.text
def test_flash_log_strings_default_does_not_warn(
generate_main: Callable[[str | Path], str],
caplog: pytest.LogCaptureFixture,
) -> None:
generate_main("tests/component_tests/logger/test_logger.yaml")
assert "esp8266_store_log_strings_in_flash" not in caplog.text
@@ -0,0 +1,9 @@
---
esphome:
name: test
esp8266:
board: d1_mini_lite
logger:
esp8266_store_log_strings_in_flash: false
+3
View File
@@ -5,7 +5,10 @@ esphome:
- audio_dac.mute_on:
- audio_dac.set_volume:
volume: 50%
- audio_dac.set_volume:
volume: !lambda return id(aic3204_dac).volume() * 0.5f;
audio_dac:
- platform: aic3204
id: aic3204_dac
i2c_id: i2c_bus
+14 -3
View File
@@ -38,14 +38,25 @@ binary_sensor:
args: ['x.has_value() ? ONOFF(x) : "Unknown"']
- binary_sensor.invalidate_state: binary_sensor_some_binary_sensor
- binary_sensor.template.publish:
id: publish_target
id: binary_sensor_publish_target
state: true
- binary_sensor.template.publish:
id: publish_target
id: binary_sensor_publish_target
state: !lambda "return x.value_or(false);"
- if:
condition:
binary_sensor.is_on: publish_target
then:
- logger.log: "publish_target is on"
- if:
condition:
binary_sensor.is_off:
id: publish_target
then:
- logger.log: "publish_target is off"
- platform: template
id: publish_target
id: binary_sensor_publish_target
name: "Publish Target"
# Test autorepeat with default configuration (no timings)
@@ -5,7 +5,8 @@ namespace esphome::climate_ir::testing {
class TestClimateIR : public ClimateIR {
public:
TestClimateIR() : ClimateIR(16.0f, 30.0f) {}
explicit TestClimateIR(bool supports_dry = false, bool supports_fan_only = false)
: ClimateIR(16.0f, 30.0f, 1.0f, supports_dry, supports_fan_only) {}
using ClimateIR::traits;
@@ -53,4 +54,30 @@ TEST(ClimateIRTest, HeatAndCoolModesFollowTheirOwnFlags) {
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_OFF));
}
TEST(ClimateIRTest, DefaultModes) {
TestClimateIR climate;
auto traits = climate.traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_OFF));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_COOL));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_DRY));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_FAN_ONLY));
}
TEST(ClimateIRTest, DryAndFanOnlyFromConstructor) {
TestClimateIR climate(true, true);
auto traits = climate.traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_DRY));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_FAN_ONLY));
}
TEST(ClimateIRTest, SetterCanTurnAModeBackOn) {
TestClimateIR climate;
climate.set_supports_cool(false);
EXPECT_FALSE(climate.traits().supports_mode(climate::CLIMATE_MODE_COOL));
climate.set_supports_cool(true);
EXPECT_TRUE(climate.traits().supports_mode(climate::CLIMATE_MODE_COOL));
}
} // namespace esphome::climate_ir::testing
+12
View File
@@ -1,20 +1,32 @@
esphome:
on_boot:
then:
- dfplayer.play_mp3: 3
- dfplayer.play_mp3: !lambda "return id(dfplayer_test).is_playing() ? 2 : 1;"
- dfplayer.play: 5
- dfplayer.play:
file: 4
loop: true
- dfplayer.play:
file: !lambda "return id(dfplayer_test).is_playing() ? 2 : 1;"
loop: !lambda return !id(dfplayer_test).is_playing();
- dfplayer.play_folder:
folder: 1
file: 3
- dfplayer.play_folder:
folder: 1
loop: true
- dfplayer.play_folder:
folder: !lambda "return id(dfplayer_test).is_playing() ? 2 : 1;"
file: !lambda "return id(dfplayer_test).is_playing() ? 4 : 3;"
loop: !lambda return !id(dfplayer_test).is_playing();
- dfplayer.set_device:
device: TF_CARD
- dfplayer.set_device: USB
- dfplayer.set_volume: 5
- dfplayer.set_volume: !lambda "return id(dfplayer_test).is_playing() ? 10 : 5;"
- dfplayer.set_eq: ROCK
- dfplayer.set_eq: !lambda "return id(dfplayer_test).is_playing() ? dfplayer::JAZZ : dfplayer::BASS;"
- dfplayer.play_next
- dfplayer.play_previous
- dfplayer.reset
+21 -3
View File
@@ -96,6 +96,20 @@ button:
- fan.turn_on:
id: test_fan
speed: !lambda 'return 1;'
- platform: template
name: "Fan Is On Off"
on_press:
- if:
condition:
fan.is_on: test_fan
then:
- fan.turn_off: test_fan
- if:
condition:
fan.is_off:
id: test_fan
then:
- fan.turn_on: test_fan
# Exercise fan.turn_on inside triggers with non-empty Ts:
# - number.on_value: Ts = float (Python value type; previously raised
@@ -111,6 +125,10 @@ number:
step: 1
on_value:
then:
- fan.turn_on:
id: test_fan
speed: !lambda "return (int) x;"
- if:
condition:
fan.is_on: test_fan
then:
- fan.turn_on:
id: test_fan
speed: !lambda "return (int) x;"
+11
View File
@@ -68,6 +68,17 @@ esphome:
- light.toggle: test_binary_light
- light.turn_off: test_rgb_light
- if:
condition:
light.is_on: test_binary_light
then:
- logger.log: binary light is on
- if:
condition:
light.is_off:
id: test_rgb_light
then:
- logger.log: rgb light is off
- light.turn_on:
id: test_rgb_light
brightness: 100%
+10
View File
@@ -4,6 +4,16 @@ esphome:
- lock.lock: test_lock1
- lock.unlock: test_lock1
- lock.open: test_lock1
- if:
condition:
lock.is_locked: test_lock1
then:
- logger.log: locked
- if:
condition:
lock.is_unlocked: test_lock1
then:
- logger.log: unlocked
output:
- platform: gpio
+2
View File
@@ -560,6 +560,8 @@ lvgl:
#endif
return std::string(buf);
align: top_left
text_letter_space: -4
text_line_space: -4
- container:
align: center
anim_duration: 1s
+11
View File
@@ -84,3 +84,14 @@ esphome:
id: online_rgba_image
url: !lambda |-
return str_sprintf("http://homeassistant.local:8123");
- online_image.set_url:
id: online_rgba_image
url: http://www.example.org/example.png
update: false
- online_image.set_url:
id: online_rgba_image
url: http://www.example.org/example.png
update: !lambda return id(online_rgba_image).get_width() > 0;
- online_image.release: online_rgba_image
- online_image.release:
id: online_rgb24_image
+1 -1
View File
@@ -18,7 +18,7 @@ sensor:
value: 10
- sensor.rotary_encoder.set_value:
id: rotary_encoder1
value: !lambda "return -1;"
value: !lambda "return -static_cast<int>(x);"
on_clockwise:
- logger.log: Clockwise
on_anticlockwise:
+3 -3
View File
@@ -7,15 +7,15 @@ sensor:
update_interval: 1s
on_value:
- sensor.template.publish:
id: publish_target
id: sensor_publish_target
state: 42.0
- sensor.template.publish:
id: publish_target
id: sensor_publish_target
state: !lambda "return x * 2.0;"
- platform: template
name: "Publish Target"
id: publish_target
id: sensor_publish_target
# Streaming filters (window_size == send_every) - uses StreamingFilter base class
- platform: copy
+10
View File
@@ -29,3 +29,13 @@ esphome:
- switch.control:
id: the_switch
state: !lambda return (1 > 2);
- if:
condition:
switch.is_on: the_switch
then:
- logger.log: "Switch is on"
- if:
condition:
switch.is_off: the_switch
then:
- logger.log: "Switch is off"
+9 -3
View File
@@ -551,11 +551,17 @@ text:
update_interval: 1s
lambda: |
return std::string{"Hello!"};
# Exercise a register_apply_condition inside a trigger with a std::string
# argument, so ApplyCondition<std::string> is compiled.
set_action:
then:
- logger.log:
format: Template Text set to %s
args: ["x.c_str()"]
- if:
condition:
cover.is_open: template_cover_with_triggers
then:
- logger.log:
format: Template Text set to %s
args: ["x.c_str()"]
alarm_control_panel:
- platform: template
+17
View File
@@ -32,6 +32,23 @@ text_sensor:
filters:
- map:
- ON -> Active
# text_sensor.state with a constant and with a lambda, inside a trigger
# whose argument is a std::string.
on_value:
- if:
condition:
text_sensor.state:
id: test_substitute_single
state: Error
then:
- logger.log: substitute reports an error
- if:
condition:
text_sensor.state:
id: test_substitute_single
state: !lambda return x;
then:
- logger.log: substitute matches the mapped value
- platform: template
name: "Test Map Multiple"
+9
View File
@@ -12,3 +12,12 @@ time:
- logger.log: "CronTrigger fired (every 10 seconds)"
- platform: sntp
id: sntp_time
esphome:
on_boot:
- if:
condition:
time.has_time:
id: sntp_time
then:
- logger.log: "sntp_time has valid time"
+119 -6
View File
@@ -10,6 +10,7 @@ import pytest
from esphome.automation import (
ApplyAction,
ApplyCall,
ApplyCondition,
ApplyField,
CallbackAutomation,
TriggerForwarder,
@@ -17,8 +18,10 @@ from esphome.automation import (
TriggerOnTrueForwarder,
build_callback_automations,
has_non_synchronous_actions,
literal_with_length,
maybe_simple_id,
register_apply_action,
register_apply_condition,
register_bare_action,
register_bare_condition,
register_parented_action,
@@ -602,6 +605,20 @@ def test_shared_builders_keep_synchronous_flag(
assert actions["my.parented"].synchronous is synchronous
async def _run_entry(
entry: RegistryEntry,
config: dict[str, object],
args: list[tuple[object, str]] | None,
platform: str,
) -> RegistryEntry:
"""Run a registered builder with the given config, trigger args and platform."""
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
args = args or []
template_arg = cg.TemplateArguments(*(t for t, _ in args))
await entry.fun({CONF_ID: PARENT_ID, **config}, ID("obj_1"), template_arg, args)
return entry
async def _run_apply_action(
registries: tuple[Registry, Registry],
fields: tuple[ApplyField | ApplyCall, ...],
@@ -611,14 +628,22 @@ async def _run_apply_action(
platform: str = "esp32",
) -> RegistryEntry:
"""Register an apply action and run its builder with the given config."""
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
actions, _ = registries
register_apply_action("my.apply", None, *fields, call=call)
entry = actions["my.apply"]
args = args or []
template_arg = cg.TemplateArguments(*(t for t, _ in args))
await entry.fun({CONF_ID: PARENT_ID, **config}, ID("obj_1"), template_arg, args)
return entry
return await _run_entry(actions["my.apply"], config, args, platform)
async def _run_apply_condition(
registries: tuple[Registry, Registry],
check: str | ApplyCall,
config: dict[str, object],
args: list[tuple[object, str]] | None = None,
platform: str = "esp32",
) -> RegistryEntry:
"""Register an apply condition and run its builder with the given config."""
_, conditions = registries
register_apply_condition("my.check", None, check)
return await _run_entry(conditions["my.check"], config, args, platform)
def _apply_lambda(mock_cg: MockCodegen) -> str:
@@ -774,6 +799,13 @@ def test_apply_registration_checks(registries: tuple[Registry, Registry]) -> Non
register_apply_action("my.ok", schema, ApplyField("kp", "set_kp", cg.float_))
with pytest.raises(ValueError, match="'kd' is not in the schema"):
register_apply_action("my.bad", schema, ApplyField("kd", "set_kd", cg.float_))
register_apply_condition(
"my.is", schema, ApplyCall("kp == {}", (("kp", cg.float_),))
)
with pytest.raises(ValueError, match="'kd' is not in the schema"):
register_apply_condition(
"my.bad_is", schema, ApplyCall("kd == {}", (("kd", cg.float_),))
)
either = cv.Any(schema, cv.Schema({cv.Optional("kd"): cv.float_}))
register_apply_action("my.any", either, ApplyField("kd", "set_kd", cg.float_))
for wrapped in (
@@ -805,3 +837,84 @@ async def test_apply_string_constant_stays_in_flash_on_esp8266(
assert f'::{PARENT_OBJ}->play(progmem_string(ESPHOME_F("a:b")));' in _apply_lambda(
mock_cg
)
@pytest.mark.asyncio
@pytest.mark.parametrize("platform", ["esp32", "esp8266"])
async def test_apply_literal_with_length_is_plain_on_every_platform(
registries: tuple[Registry, Registry], mock_cg: MockCodegen, platform: str
) -> None:
"""A (const char *, size_t) target gets the RAM literal and its byte length, never a flash copy."""
fields = (
ApplyField("option", "set_option", cg.std_string, const_fn=literal_with_length),
)
await _run_apply_action(
registries, fields, {"option": "h\u00e9llo"}, platform=platform
)
text = _apply_lambda(mock_cg)
assert f'::{PARENT_OBJ}->set_option("h\\303\\251llo", 6);' in text
assert "progmem_string" not in text
@pytest.mark.asyncio
async def test_register_apply_condition_predicate(
registries: tuple[Registry, Registry], mock_cg: MockCodegen
) -> None:
entry = await _run_apply_condition(
registries, "is_playing()", {}, args=[(cg.int32, "x")]
)
assert entry.type_id is ApplyCondition
condition_id, template_arg, check = mock_cg.new_pvariable.call_args.args
assert condition_id == ID("obj_1")
assert str(template_arg) == "<int32_t>"
text = str(check)
assert text.startswith("[](const std::remove_cvref_t<int32_t> & x) -> bool {")
assert f"return ::{PARENT_OBJ}->is_playing();" in text
@pytest.mark.asyncio
async def test_apply_condition_compares_config_value(
registries: tuple[Registry, Registry], mock_cg: MockCodegen
) -> None:
check = ApplyCall("state == {}", (("state", cg.bool_),))
await _run_apply_condition(registries, check, {"state": True})
assert f"return ::{PARENT_OBJ}->state == true;" in _apply_lambda(mock_cg)
with pytest.raises(EsphomeError, match="needs all of"):
await _run_apply_condition(registries, check, {})
@pytest.mark.asyncio
@pytest.mark.parametrize("platform", ["esp32", "esp8266"])
async def test_apply_condition_string_constant_is_a_plain_literal(
registries: tuple[Registry, Registry], mock_cg: MockCodegen, platform: str
) -> None:
check = ApplyCall("state == {}", (("state", cg.std_string),))
await _run_apply_condition(registries, check, {"state": "two"}, platform=platform)
assert f'return ::{PARENT_OBJ}->state == "two";' in _apply_lambda(mock_cg)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("body", "expected", "called"),
[
("return x;", "->state == (x);", False),
('return x.empty() ? "e" : x;', '->state == (x.empty() ? "e" : x);', False),
('if (x.empty()) return "e";\nreturn x;', "}(x);", True),
],
)
async def test_apply_condition_string_lambda_paths(
registries: tuple[Registry, Registry],
mock_cg: MockCodegen,
body: str,
expected: str,
called: bool,
) -> None:
"""A single return is inlined with no copy; a longer body is a called std::string lambda."""
check = ApplyCall("state == {}", (("state", cg.std_string),))
await _run_apply_condition(
registries, check, {"state": Lambda(body)}, args=[(cg.std_string, "x")]
)
text = _apply_lambda(mock_cg)
assert expected in text
assert ("-> std::string {" in text) is called