Merge remote-tracking branch 'origin/dev' into sscanf_stub

# Conflicts:
#	esphome/components/esp32/__init__.py
#	esphome/core/defines.h
This commit is contained in:
J. Nick Koston
2026-10-09 16:08:43 -10:00
93 changed files with 1188 additions and 958 deletions
+25 -14
View File
@@ -264,13 +264,17 @@ def string_ref_literal(config: ConfigType, value: str) -> str:
return f"StringRef({literal_with_length(config, value)})"
def _names_parent(text: str) -> bool:
return any(f == "parent" for _, f, _, _ in string.Formatter().parse(text))
@dataclass(frozen=True)
class ApplyCall:
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
path into nested sections. ``target`` and a plain ``str`` ``type_`` (raw C++ type text) may
name the parent object as ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
id bypasses it. The statement is skipped when none of its keys is set, always emitted when it
has no keys, and a partial set is a config error.
"""
@@ -282,13 +286,13 @@ class ApplyCall:
fields = [
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
]
if any(fields):
if any(f not in ("", "parent") for f in fields):
raise ValueError(
f"apply target {self.target!r}: only bare {{}} placeholders"
f"apply target {self.target!r}: only {{}} and {{parent}} placeholders"
)
if len(fields) != len(self.args):
if (count := fields.count("")) != len(self.args):
raise ValueError(
f"apply target {self.target!r} has {len(fields)} "
f"apply target {self.target!r} has {count} "
f"placeholder(s) for {len(self.args)} config key(s)"
)
if any(len(arg) not in (2, 3) for arg in self.args):
@@ -296,6 +300,12 @@ class ApplyCall:
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
)
@property
def names_parent(self) -> bool:
return _names_parent(self.target) or any(
isinstance(arg[1], str) and _names_parent(arg[1]) for arg in self.args
)
@property
def members(self) -> list[tuple[Any, Any, Any]]:
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
@@ -494,7 +504,7 @@ def register_apply_action(
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args
)
statements.append(f"{receiver}{target.format(*exprs)};")
statements.append(f"{receiver}{target.format(*exprs, parent=parent)};")
if call:
statements = [
f"auto apply_call = {parent}->{call}();",
@@ -526,7 +536,7 @@ async def _render_check(
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args, compare=True
)
return target.format(*exprs)
return target.format(*exprs, parent=parent)
def register_apply_condition(
@@ -1061,21 +1071,22 @@ async def build_trigger_callback(
params: TemplateArgsType,
forward: Sequence[str | Expression] | None = None,
when: str | ApplyCall | None = None,
parent: MockObj | None = None,
) -> LambdaExpression:
"""Build the Automation for ``config`` and return a stateless callback that triggers it.
``params`` are the parent callback's parameters, ``forward`` the expressions passed to
``trigger()`` (default: the parameter names; write the parent as ``parent_ref(var)``),
``when`` a filter the callback returns early on, skipped like any ``ApplyCall`` when none
of its keys is set.
of its keys is set. ``when`` may name ``parent`` as ``{parent}``, e.g.
``"{parent}->is_fully_open()"``.
"""
members: list[tuple[Any, Any, Any]] = []
if when is not None:
call = when if isinstance(when, ApplyCall) else ApplyCall(when)
members = call.members
# A trigger callback has no parent for a str type to name.
if any(isinstance(t, str) and "{parent}" in t for _, t, _ in members):
raise ValueError(f"trigger filter {call.target!r}: a type names {{parent}}")
if parent is None and call.names_parent:
raise ValueError(f"trigger filter {call.target!r} names {{parent}}")
obj = await _new_automation(args, config)
lambda_args = _apply_lambda_args(params)
statements: list[str] = []
@@ -1088,7 +1099,7 @@ async def build_trigger_callback(
members,
values,
config,
None,
None if parent is None else str(parent_ref(parent)),
lambda_args,
)
statements.append(f"if (!({check}))\n return;")
@@ -1137,7 +1148,7 @@ async def build_callback_automation(
"forwarder cannot be combined with params, forward or when"
)
callback = await build_trigger_callback(
args, config, args if params is None else params, forward, when
args, config, args if params is None else params, forward, when, parent
)
cg.add(getattr(parent, callback_method)(callback))
return
+13 -4
View File
@@ -425,15 +425,24 @@ project({CORE.name})
# Emit per-memory-type JSON size data for ESPHome to read post-build.
# json2 stays small; raw dumps every symbol (~2s on a large map) and
# this command runs inside the link edge, blocking everything downstream.
# The map is a BYPRODUCT so ninja knows the link writes it; IDF's size
# target depends on the map and can then be built in the same run as all.
# IDF's cmakev2 declares the map itself, so drop this line on that switch.
# IDF's size target depends on the map, so ninja has to know the link
# writes it for size to build in the same run as all. IDF declares the
# map itself since espressif/esp-idf#19201 (and in cmakev2), and ninja
# rejects two rules for one output, so declare it here only when IDF has
# not. add_custom_command(OUTPUT) marks its outputs GENERATED.
get_source_file_property(esphome_map_declared
${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map GENERATED)
if(esphome_map_declared)
set(esphome_map_byproducts)
else()
set(esphome_map_byproducts BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map)
endif()
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map
${{esphome_map_byproducts}}
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
VERBATIM
)
+1
View File
@@ -34,6 +34,7 @@ from esphome.cpp_generator import ( # noqa: F401
extern_progmem_array,
get_variable,
get_variable_with_full_id,
is_static_pointer,
is_template,
new_Pvariable,
new_variable,
+1 -1
View File
@@ -760,7 +760,7 @@ message ListEntitiesLightResponse {
bool legacy_supports_color_temperature = 8 [deprecated=true];
float min_mireds = 9;
float max_mireds = 10;
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
repeated string effects = 11 [(container_pointer_no_template) = "light::LightEffectNames"];
bool disabled_by_default = 13;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 15;
+1 -10
View File
@@ -644,16 +644,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
msg.min_mireds = traits.get_min_mireds();
msg.max_mireds = traits.get_max_mireds();
}
FixedVector<const char *> effects_list;
if (light->supports_effects()) {
auto &light_effects = light->get_effects();
effects_list.init(light_effects.size() + 1);
effects_list.push_back("None");
for (auto *effect : light_effects) {
// c_str() is safe as effect names are null-terminated strings from codegen
effects_list.push_back(effect->get_name().c_str());
}
}
light::LightEffectNames effects_list(light->get_effects());
msg.effects = &effects_list;
return fill_and_encode_entity_info(light, msg, conn, remaining_size);
}
+4 -4
View File
@@ -688,8 +688,8 @@ uint8_t *ListEntitiesLightResponse::encode_msg(const void *self, uint8_t *__rest
if (uint32_t raw = float_to_raw(msg.max_mireds); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 85, raw);
}
for (const char *it : *msg.effects) {
pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it, strlen(it));
for (const auto &it : *msg.effects) {
pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it);
}
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.disabled_by_default);
#ifdef USE_ENTITY_ICON
@@ -713,8 +713,8 @@ uint32_t ListEntitiesLightResponse::calc_size_msg(const void *self) {
size += ProtoSize::calc_float(1, msg.min_mireds);
size += ProtoSize::calc_float(1, msg.max_mireds);
if (!msg.effects->empty()) {
for (const char *it : *msg.effects) {
size += ProtoSize::calc_length_force(1, strlen(it));
for (const auto &it : *msg.effects) {
size += ProtoSize::calc_length_force(1, it.size());
}
}
size += ProtoSize::calc_bool(1, msg.disabled_by_default);
+1 -1
View File
@@ -1059,7 +1059,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
const light::ColorModeMask *supported_color_modes{};
float min_mireds{0.0f};
float max_mireds{0.0f};
const FixedVector<const char *> *effects{};
const light::LightEffectNames *effects{};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -15,6 +15,7 @@
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_effect_names.h"
#include "esphome/components/light/light_traits.h"
#endif
@@ -36,19 +36,6 @@ CONFIG_SCHEMA = cv.All(
)
def _subtree_references_uart(node: object, uart_id: str) -> bool:
"""Return True if any dict in the subtree has a uart_id entry naming this bus."""
if isinstance(node, dict):
return any(
(key == CONF_UART_ID and str(value) == uart_id)
or _subtree_references_uart(value, uart_id)
for key, value in node.items()
)
if isinstance(node, list):
return any(_subtree_references_uart(item, uart_id) for item in node)
return False
def _reject_debug(uart_conf: ConfigType) -> ConfigType:
# The worker tasks use the IDF driver directly, so the uart debugger never sees
# bridge traffic and its dummy_receiver would drain RX bytes on the main loop.
@@ -86,7 +73,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
for domain, domain_conf in full_config.items():
if domain == BRIDGE_DOMAIN:
continue
if _subtree_references_uart(domain_conf, owned_id):
if uart.subtree_references_uart(domain_conf, owned_id):
raise cv.Invalid(
f"The {label} '{owned_id}' is also used by '{domain}'; a bridge "
f"requires exclusive use of its {label}.",
+10 -10
View File
@@ -43,9 +43,6 @@ DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase)
DateTimeStateTrigger = datetime_ns.class_(
"DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
)
OnTimeTrigger = datetime_ns.class_(
"OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity)
@@ -75,11 +72,7 @@ def _validate_time_present(config: ConfigType) -> ConfigType:
_DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Schema(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
}
),
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
@@ -144,9 +137,16 @@ async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
# The state callback carries nothing; the automation gets the value as an ESPTime.
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)
await automation.build_callback_automation(
var,
"add_on_state_callback",
[(cg.ESPTime, "x")],
conf,
params=[],
forward=[automation.parent_ref(var).state_as_esptime()],
)
if CONF_TIME_ID in config:
rtc = await cg.get_variable(config[CONF_TIME_ID])
@@ -31,14 +31,4 @@ class DateTimeBase : public EntityBase {
#endif
};
class DateTimeStateTrigger final : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) {
parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); });
}
protected:
DateTimeBase *parent_;
};
} // namespace esphome::datetime
@@ -20,9 +20,10 @@ from esphome.const import (
CONF_ON_VALUE,
CONF_SWITCH,
CONF_TEXT,
CONF_TRIGGER_ID,
CONF_TYPE,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@numo68"]
DOMAIN = "display_menu_base"
@@ -86,26 +87,6 @@ MENU_MODES = {
CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK,
}
DisplayMenuOnEnterTrigger = display_menu_base_ns.class_(
"DisplayMenuOnEnterTrigger", automation.Trigger
)
DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_(
"DisplayMenuOnLeaveTrigger", automation.Trigger
)
DisplayMenuOnValueTrigger = display_menu_base_ns.class_(
"DisplayMenuOnValueTrigger", automation.Trigger
)
DisplayMenuOnNextTrigger = display_menu_base_ns.class_(
"DisplayMenuOnNextTrigger", automation.Trigger
)
DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
"DisplayMenuOnPrevTrigger", automation.Trigger
)
def validate_format(format):
if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None:
@@ -129,44 +110,20 @@ MENU_ITEM_COMMON_SCHEMA = cv.Schema(
MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnEnterTrigger
),
}
),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnLeaveTrigger
),
}
),
cv.Optional(CONF_ON_ENTER): automation.validate_automation({}),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}),
}
)
MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnValueTrigger
),
}
),
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
}
)
MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnValueTrigger
),
}
),
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
}
)
@@ -230,20 +187,8 @@ MENU_ITEM_SCHEMA = cv.typed_schema(
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_ON_NEXT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnNextTrigger
),
}
),
cv.Optional(CONF_ON_PREV): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnPrevTrigger
),
}
),
cv.Optional(CONF_ON_NEXT): automation.validate_automation({}),
cv.Optional(CONF_ON_PREV): automation.validate_automation({}),
}
),
},
@@ -256,20 +201,8 @@ DISPLAY_MENU_BASE_SCHEMA = cv.Schema(
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu),
cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES),
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnEnterTrigger
),
}
),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnLeaveTrigger
),
}
),
cv.Optional(CONF_ON_ENTER): automation.validate_automation({}),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}),
cv.Required(CONF_ITEMS): cv.All(
cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1)
),
@@ -302,6 +235,24 @@ automation.register_apply_condition(
)
# Each item callback carries nothing; the automation receives the item itself.
_ITEM_CALLBACKS = (
(CONF_ON_ENTER, "add_on_enter_callback"),
(CONF_ON_LEAVE, "add_on_leave_callback"),
(CONF_ON_VALUE, "add_on_value_callback"),
(CONF_ON_NEXT, "add_on_next_callback"),
(CONF_ON_PREV, "add_on_prev_callback"),
)
async def _build_item_automations(item: MockObj, config: ConfigType) -> None:
for conf_key, callback_method in _ITEM_CALLBACKS:
for conf in config.get(conf_key, []):
await automation.build_parent_callback_automation(
item, callback_method, (MenuItemConstPtr, "it"), conf
)
async def menu_item_to_code(menu, config, parent):
if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES:
item = cg.new_Pvariable(config[CONF_ID])
@@ -340,21 +291,7 @@ async def menu_item_to_code(menu, config, parent):
cg.add(item.set_switch_variable(var))
cg.add(item.set_on_text(config[CONF_ON_TEXT]))
cg.add(item.set_off_text(config[CONF_OFF_TEXT]))
for conf in config.get(CONF_ON_ENTER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_LEAVE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_NEXT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_PREV, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
await _build_item_automations(item, config)
async def display_menu_to_code(menu, config):
@@ -364,9 +301,4 @@ async def display_menu_to_code(menu, config):
cg.add(menu.set_mode(config[CONF_MODE]))
for c in config[CONF_ITEMS]:
await menu_item_to_code(menu, c, root_item)
for conf in config.get(CONF_ON_ENTER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_LEAVE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
await _build_item_automations(root_item, config)
@@ -1,58 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "display_menu_base.h"
namespace esphome::display_menu_base {
class DisplayMenuOnEnterTrigger final : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
parent->add_on_enter_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItem *parent_;
};
class DisplayMenuOnLeaveTrigger final : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) {
parent->add_on_leave_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItem *parent_;
};
class DisplayMenuOnValueTrigger final : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) {
parent->add_on_value_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItem *parent_;
};
class DisplayMenuOnNextTrigger final : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) {
parent->add_on_next_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItemCustom *parent_;
};
class DisplayMenuOnPrevTrigger final : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) {
parent->add_on_prev_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItemCustom *parent_;
};
} // namespace esphome::display_menu_base
+1 -3
View File
@@ -4001,11 +4001,9 @@ def process_stacktrace(config, line, backtrace_state):
return backtrace_state
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
# Each stub only compiles under its define, so keep the sources out of other builds
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"gpio.cpp": "USE_ESP32_INTERNAL_GPIO",
"sscanf_stubs.cpp": "USE_ESP32_SSCANF_STUB",
"vasprintf_stubs.cpp": "USE_ESP32_VASPRINTF_STUB",
}
+6 -5
View File
@@ -1,9 +1,10 @@
#include "esphome/core/defines.h"
// Also defines the core ISRInternalGPIOPin methods; those are only reachable
// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely.
#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO)
// Always built: InternalGPIOPin is this class, so components that merely hold a
// pointer to it reference these methods even in configs without a pin.
#ifdef USE_ESP32
#include "gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
@@ -68,7 +69,7 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
return ISRInternalGPIOPin((void *) arg);
}
void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
void ESP32InternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
@@ -207,4 +208,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
} // namespace esphome
#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO
#endif // USE_ESP32
+12 -7
View File
@@ -1,7 +1,8 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/core/hal.h"
#include "esphome/core/gpio_pin.h"
#include <esp_system.h>
#include <driver/gpio.h>
namespace esphome::esp32 {
@@ -10,7 +11,7 @@ namespace esphome::esp32 {
static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t");
static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field");
class ESP32InternalGPIOPin final : public InternalGPIOPin {
class ESP32InternalGPIOPin final : public GPIOPin {
public:
void set_pin(gpio_num_t pin) { this->pin_ = static_cast<uint8_t>(pin); }
void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; }
@@ -24,11 +25,15 @@ class ESP32InternalGPIOPin final : public InternalGPIOPin {
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return this->pin_; }
void detach_interrupt() const;
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
ISRInternalGPIOPin to_isr() const;
uint8_t get_pin() const { return this->pin_; }
gpio::Flags get_flags() const override { return this->flags_; }
bool is_inverted() const override { return this->pin_flags_.inverted; }
bool is_inverted() const { return this->pin_flags_.inverted; }
bool is_internal() override { return true; }
gpio_num_t get_pin_num() const { return static_cast<gpio_num_t>(this->pin_); }
gpio_drive_cap_t get_drive_strength() const { return static_cast<gpio_drive_cap_t>(this->pin_flags_.drive_strength); }
#ifdef USE_GPIO_HOLD
@@ -41,7 +46,7 @@ class ESP32InternalGPIOPin final : public InternalGPIOPin {
#endif
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
// Memory layout: 8 bytes total on 32-bit systems
// - 3 bytes for members below
-1
View File
@@ -257,7 +257,6 @@ ESP32_PIN_SCHEMA = cv.All(
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
async def esp32_pin_to_code(config):
cg.add_define("USE_ESP32_INTERNAL_GPIO")
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
+2 -1
View File
@@ -1,6 +1,7 @@
#ifdef USE_ESP8266
#include "gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::esp8266 {
@@ -74,7 +75,7 @@ ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const {
return ISRInternalGPIOPin((void *) arg);
}
void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
void ESP8266GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
uint8_t arduino_mode = 0;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
+11 -7
View File
@@ -2,12 +2,12 @@
#ifdef USE_ESP8266
#include "esphome/core/hal.h"
#include "esphome/core/gpio_pin.h"
#include <Arduino.h>
namespace esphome::esp8266 {
class ESP8266GPIOPin final : public InternalGPIOPin {
class ESP8266GPIOPin final : public GPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
@@ -18,14 +18,18 @@ class ESP8266GPIOPin final : public InternalGPIOPin {
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
void detach_interrupt() const;
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
ISRInternalGPIOPin to_isr() const;
uint8_t get_pin() const { return pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; }
bool is_inverted() const { return inverted_; }
bool is_internal() override { return true; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
uint8_t pin_;
bool inverted_{};
@@ -13,7 +13,6 @@ from esphome.const import (
CONF_FONT,
CONF_FOREGROUND_COLOR,
CONF_ID,
CONF_TRIGGER_ID,
)
from esphome.types import ConfigType
@@ -29,9 +28,6 @@ MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArgument
MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator(
"const"
)
GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_(
"GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger
)
CODEOWNERS = ["@MrMDavidson"]
DOMAIN = "graphical_display_menu"
@@ -49,13 +45,7 @@ CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string),
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_ON_REDRAW): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
GraphicalDisplayMenuOnRedrawTrigger
)
}
),
cv.Optional(CONF_ON_REDRAW): automation.validate_automation({}),
}
)
)
@@ -92,9 +82,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_background_color(background_color))
for conf in config.get(CONF_ON_REDRAW, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf
await automation.build_callback_automation(
var, "add_on_redraw_callback", [(GraphicalDisplayMenuConstPtr, "it")], conf
)
await display_menu_to_code(var, config)
@@ -241,6 +241,6 @@ void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, co
"draw_item should be called.");
}
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); }
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(this); }
} // namespace esphome::graphical_display_menu
@@ -70,17 +70,7 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen
Color foreground_color_{COLOR_ON};
Color background_color_{COLOR_OFF};
CallbackManager<void()> on_redraw_callbacks_{};
};
class GraphicalDisplayMenuOnRedrawTrigger final : public Trigger<const GraphicalDisplayMenu *> {
public:
explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) {
parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); });
}
protected:
GraphicalDisplayMenu *parent_;
LazyCallbackManager<void(const GraphicalDisplayMenu *)> on_redraw_callbacks_{};
};
} // namespace graphical_display_menu
+6 -3
View File
@@ -187,15 +187,18 @@ void HeatpumpIRClimate::transmit_state() {
swing_h_cmd = HDIR_SWING;
}
// MitsubishiMSCHeatpumpIR only knows FAN_1..FAN_3 and sends anything else as auto,
// so low/medium/high have to start at FAN_1 there instead of FAN_2.
const bool three_speed_fan = this->protocol_ == PROTOCOL_MITSUBISHI_MSC;
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
fan_speed_cmd = FAN_2;
fan_speed_cmd = three_speed_fan ? FAN_1 : FAN_2;
break;
case climate::CLIMATE_FAN_MEDIUM:
fan_speed_cmd = FAN_3;
fan_speed_cmd = three_speed_fan ? FAN_2 : FAN_3;
break;
case climate::CLIMATE_FAN_HIGH:
fan_speed_cmd = FAN_4;
fan_speed_cmd = three_speed_fan ? FAN_3 : FAN_4;
break;
case climate::CLIMATE_FAN_AUTO:
default:
+2 -1
View File
@@ -1,6 +1,7 @@
#ifdef USE_HOST
#include "gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -20,7 +21,7 @@ ISRInternalGPIOPin HostGPIOPin::to_isr() const {
return ISRInternalGPIOPin((void *) arg);
}
void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
void HostGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type);
}
void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); }
+11 -7
View File
@@ -2,11 +2,11 @@
#ifdef USE_HOST
#include "esphome/core/hal.h"
#include "esphome/core/gpio_pin.h"
namespace esphome::host {
class HostGPIOPin final : public InternalGPIOPin {
class HostGPIOPin final : public GPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
@@ -17,14 +17,18 @@ class HostGPIOPin final : public InternalGPIOPin {
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
void detach_interrupt() const;
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
ISRInternalGPIOPin to_isr() const;
uint8_t get_pin() const { return pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; }
bool is_inverted() const { return inverted_; }
bool is_internal() override { return true; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
uint8_t pin_;
bool inverted_{};
@@ -1,6 +1,7 @@
#ifdef USE_LIBRETINY
#include "gpio_arduino.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::libretiny {
@@ -37,7 +38,7 @@ ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const {
return ISRInternalGPIOPin((void *) arg);
}
void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
PinStatus arduino_mode = (PinStatus) 255;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
+11 -7
View File
@@ -1,11 +1,11 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/hal.h"
#include "esphome/core/gpio_pin.h"
namespace esphome::libretiny {
class ArduinoInternalGPIOPin final : public InternalGPIOPin {
class ArduinoInternalGPIOPin final : public GPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
@@ -16,14 +16,18 @@ class ArduinoInternalGPIOPin final : public InternalGPIOPin {
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
void detach_interrupt() const;
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
ISRInternalGPIOPin to_isr() const;
uint8_t get_pin() const { return pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; }
bool is_inverted() const { return inverted_; }
bool is_internal() override { return true; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
uint8_t pin_;
bool inverted_{};
+14 -2
View File
@@ -41,7 +41,7 @@ from esphome.core.entity_helpers import (
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -71,6 +71,7 @@ from .types import ( # noqa: F401
ChannelColors,
ColorMode,
GammaTable,
LightEffect,
LightOutput,
LightState,
LightStateRTCState,
@@ -519,6 +520,17 @@ def validate_color_temperature_channels(value):
return value
def _effects_table(effects: list[MockObj]) -> MockObj:
"""Emit the flash table of a light's effect pointers; each must be a static address."""
return cg.shared_progmem_array(
"light_effects",
LightEffect.operator("ptr"),
cg.ArrayInitializer(*effects),
share=False,
constexpr=False,
)
@setup_entity("light")
async def setup_light_core_(light_var, config, output_var):
# All 8 legacy restore_mode values, and the restore_state key, are just different
@@ -586,7 +598,7 @@ async def setup_light_core_(light_var, config, output_var):
EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
)
if effects:
cg.add(light_var.add_effects(effects))
cg.add(light_var.add_effects(_effects_table(effects), len(effects)))
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
+1 -1
View File
@@ -147,7 +147,7 @@ void LightCall::perform() {
// EFFECT
StringRef effect_s;
if (this->effect_ == 0u) {
effect_s = StringRef::from_lit("None");
effect_s = EFFECT_NONE_REF;
} else {
effect_s = this->parent_->effects_[this->effect_ - 1]->get_name();
}
+2
View File
@@ -5,6 +5,8 @@
namespace esphome::light {
inline constexpr StringRef EFFECT_NONE_REF = StringRef::from_lit("None");
class LightState;
class LightEffect {
@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#include "light_effect.h"
namespace esphome::light {
/// Effect names as the native API lists them: "None" first, then each effect; empty without effects.
/// Reads the light's effect table directly, so listing entities allocates nothing. Holds a reference:
/// it only lives for one synchronous encode, and by value costs flash on every copy.
class LightEffectNames {
public:
explicit LightEffectNames(const ConstVector<LightEffect *> &effects) : effects_(effects) {}
class Iterator {
public:
ESPHOME_ALWAYS_INLINE Iterator(const ConstVector<LightEffect *> &effects, size_t index)
: effects_(effects), index_(index) {}
ESPHOME_ALWAYS_INLINE StringRef operator*() const {
return this->index_ == 0 ? EFFECT_NONE_REF : this->effects_[this->index_ - 1]->get_name();
}
ESPHOME_ALWAYS_INLINE Iterator &operator++() {
++this->index_;
return *this;
}
ESPHOME_ALWAYS_INLINE bool operator!=(const Iterator &other) const { return this->index_ != other.index_; }
protected:
const ConstVector<LightEffect *> &effects_;
size_t index_;
};
ESPHOME_ALWAYS_INLINE Iterator begin() const { return {this->effects_, 0}; }
ESPHOME_ALWAYS_INLINE Iterator end() const {
return {this->effects_, this->effects_.empty() ? 0 : this->effects_.size() + 1};
}
ESPHOME_ALWAYS_INLINE bool empty() const { return this->effects_.empty(); }
protected:
const ConstVector<LightEffect *> &effects_;
};
} // namespace esphome::light
-12
View File
@@ -9,8 +9,6 @@
#include "light_output.h"
#include "transformers.h"
#include <limits>
namespace esphome::light {
ESPHOME_LOG_TAG(TAG, "light");
@@ -196,8 +194,6 @@ void LightState::publish_state() {
LightOutput *LightState::get_output() const { return this->output_; }
static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None");
StringRef LightState::get_effect_name() {
if (this->active_effect_index_ > 0) {
return this->effects_[this->active_effect_index_ - 1]->get_name();
@@ -218,11 +214,6 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen
this->target_state_reached_listeners_->push_back(listener);
}
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
// Called once from Python codegen during setup with all effects from YAML config
this->effects_ = effects;
}
void LightState::current_values_as_brightness(float *brightness) {
this->current_values.as_brightness(brightness);
*brightness = this->gamma_correct_lut(*brightness);
@@ -352,9 +343,6 @@ float LightState::gamma_uncorrect_lut(float value) const {
#endif // USE_LIGHT_GAMMA_LUT
void LightState::start_effect_(uint32_t effect_index) {
// An external add_effects() can exceed the codegen cap; ignore an index the uint16_t can't hold
if (effect_index > std::numeric_limits<uint16_t>::max())
return;
this->stop_effect_();
if (effect_index == 0)
return;
+7 -4
View File
@@ -217,10 +217,13 @@ class LightState : public EntityBase, public Component {
bool supports_effects() const { return !this->effects_.empty(); }
/// Get all effects for this light state.
const FixedVector<LightEffect *> &get_effects() const { return this->effects_; }
const ConstVector<LightEffect *> &get_effects() const { return this->effects_; }
/// Add effects for this light state.
void add_effects(const std::initializer_list<LightEffect *> &effects);
/// Codegen only: point at the generated table of this light's effects, which must outlive it.
/// Codegen caps the count at 65535, so an effect index always fits the uint16_t active index.
void add_effects(LightEffect *const *effects, size_t count) {
this->effects_ = ConstVector<LightEffect *>(effects, count);
}
/// Get the total number of effects available for this light.
size_t get_effect_count() const { return this->effects_.size(); }
@@ -354,7 +357,7 @@ class LightState : public EntityBase, public Component {
/// The currently active transformer for this light (transition/flash).
std::unique_ptr<LightTransformer> transformer_{nullptr};
/// List of effects for this light.
FixedVector<LightEffect *> effects_;
ConstVector<LightEffect *> effects_;
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
+4 -14
View File
@@ -12,7 +12,6 @@ from esphome.const import (
CONF_ID,
CONF_MICROPHONE,
CONF_ON_DATA,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
@@ -30,17 +29,12 @@ microphone_ns = cg.esphome_ns.namespace("microphone")
Microphone = microphone_ns.class_("Microphone")
MicrophoneSource = microphone_ns.class_("MicrophoneSource")
DataTrigger = microphone_ns.class_(
"DataTrigger",
automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")),
)
async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None:
for conf in config.get(CONF_ON_DATA, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
await automation.build_callback_automation(
var,
"add_data_callback",
[(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")],
conf,
)
@@ -54,11 +48,7 @@ async def register_microphone(var: MockObj, config: ConfigType) -> None:
MICROPHONE_SCHEMA = cv.Schema.extend(audio.AUDIO_COMPONENT_SCHEMA).extend(
{
cv.Optional(CONF_ON_DATA): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger),
}
),
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
}
)
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "microphone.h"
#include <vector>
namespace esphome::microphone {
class DataTrigger final : public Trigger<const std::vector<uint8_t> &> {
public:
explicit DataTrigger(Microphone *mic) {
mic->add_data_callback([this](const std::vector<uint8_t> &data) { this->trigger(data); });
}
};
} // namespace esphome::microphone
+14 -30
View File
@@ -5,7 +5,7 @@ import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
from esphome.core import CORE
from . import const, generate, schema, validate
@@ -27,14 +27,16 @@ CONF_SYNC_MODE = "sync_mode"
CONF_BEFORE_SEND = "before_send"
CONF_BEFORE_PROCESS_RESPONSE = "before_process_response"
# Triggers
BeforeSendTrigger = generate.opentherm_ns.class_(
"BeforeSendTrigger",
automation.Trigger.template(generate.OpenthermData.operator("ref")),
)
BeforeProcessResponseTrigger = generate.opentherm_ns.class_(
"BeforeProcessResponseTrigger",
automation.Trigger.template(generate.OpenthermData.operator("ref")),
_DATA_ARGS = [(generate.OpenthermData.operator("ref"), "x")]
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_BEFORE_SEND, "add_on_before_send_callback", _DATA_ARGS
),
automation.CallbackAutomation(
CONF_BEFORE_PROCESS_RESPONSE,
"add_on_before_process_response_callback",
_DATA_ARGS,
),
)
CONFIG_SCHEMA = cv.All(
@@ -51,17 +53,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
cv.Optional(CONF_BEFORE_SEND): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger),
}
),
cv.Optional(CONF_BEFORE_SEND): automation.validate_automation({}),
cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BeforeProcessResponseTrigger
),
}
{}
),
}
)
@@ -132,14 +126,4 @@ async def to_code(config: dict[str, Any]) -> None:
generate.define_setting_readers(const.SETTING, settings)
generate.add_messages(var, settings, schema.SETTINGS)
for conf in config.get(CONF_BEFORE_SEND, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
)
for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "hub.h"
#include "opentherm.h"
namespace esphome::opentherm {
class BeforeSendTrigger final : public Trigger<OpenthermData &> {
public:
BeforeSendTrigger(OpenthermHub *hub) {
hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); });
}
};
class BeforeProcessResponseTrigger final : public Trigger<OpenthermData &> {
public:
BeforeProcessResponseTrigger(OpenthermHub *hub) {
hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); });
}
};
} // namespace esphome::opentherm
+2 -1
View File
@@ -1,6 +1,7 @@
#ifdef USE_RP2
#include "gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -40,7 +41,7 @@ ISRInternalGPIOPin RP2GPIOPin::to_isr() const {
return ISRInternalGPIOPin((void *) arg);
}
void RP2GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
void RP2GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
PinStatus arduino_mode = LOW;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
+11 -7
View File
@@ -3,11 +3,11 @@
#ifdef USE_RP2
#include <Arduino.h>
#include "esphome/core/hal.h"
#include "esphome/core/gpio_pin.h"
namespace esphome::rp2 {
class RP2GPIOPin final : public InternalGPIOPin {
class RP2GPIOPin final : public GPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
@@ -18,14 +18,18 @@ class RP2GPIOPin final : public InternalGPIOPin {
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
void detach_interrupt() const;
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
ISRInternalGPIOPin to_isr() const;
uint8_t get_pin() const { return pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; }
bool is_inverted() const { return inverted_; }
bool is_internal() override { return true; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
uint8_t pin_;
bool inverted_{};
+1 -1
View File
@@ -218,7 +218,7 @@ static constexpr uint8_t PROGMEM
100 }; // 100 ms delay
// clang-format on
static const char *const TAG = "st7735";
ESPHOME_LOG_TAG(TAG, "st7735");
ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr,
bool invert_colors)
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::st7789v {
static const char *const TAG = "st7789v";
ESPHOME_LOG_TAG(TAG, "st7789v");
#ifdef USE_ESP32
static constexpr size_t TEMP_BUFFER_SIZE = 1024;
#else
+8 -15
View File
@@ -46,7 +46,6 @@ DOMAIN = "time"
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)
# C++ types for pre-parsed timezone struct generation
DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True)
@@ -357,11 +356,7 @@ TIME_SCHEMA = cv.Schema(
},
validate_cron_keys,
),
cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncTrigger),
}
),
cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation({}),
}
).extend(
# ``visibility=ADVANCED`` flags the inherited ``update_interval``
@@ -424,8 +419,7 @@ async def setup_time_core_(time_var, config):
_emit_parsed_timezone_fields(parsed)
on_time = config.get(CONF_ON_TIME, [])
on_time_sync = config.get(CONF_ON_TIME_SYNC, [])
if on_time or on_time_sync:
if on_time:
cg.add_define("USE_TIME_TRIGGERS")
for conf in on_time:
@@ -447,11 +441,10 @@ async def setup_time_core_(time_var, config):
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
for conf in on_time_sync:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_TIME_SYNC, []):
await automation.build_callback_automation(
time_var, "add_on_time_sync_callback", [], conf
)
async def register_time(time_var, config):
@@ -478,8 +471,8 @@ automation.register_apply_condition(
# posix_tz.cpp is fully #ifdef'd on USE_TIME_TIMEZONE, set only when a
# timezone is configured or detected; automation.cpp holds the on_time and
# on_time_sync triggers and is #ifdef'd on USE_TIME_TRIGGERS.
# timezone is configured or detected; automation.cpp holds the on_time
# trigger and is #ifdef'd on USE_TIME_TRIGGERS.
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"posix_tz.cpp": "USE_TIME_TIMEZONE",
-4
View File
@@ -94,10 +94,6 @@ void CronTrigger::add_days_of_week(const std::vector<uint8_t> &days_of_week) {
}
float CronTrigger::get_setup_priority() const { return setup_priority::HARDWARE; }
SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) {
rtc->add_on_time_sync_callback([this]() { this->trigger(); });
}
} // namespace esphome::time
#endif // USE_TIME_TRIGGERS
-8
View File
@@ -43,14 +43,6 @@ class CronTrigger final : public Trigger<>, public Component {
RealTimeClock *rtc_;
optional<ESPTime> last_check_;
};
class SyncTrigger final : public Trigger<>, public Component {
public:
explicit SyncTrigger(RealTimeClock *rtc);
protected:
RealTimeClock *rtc_;
};
} // namespace esphome::time
#endif // USE_TIME_TRIGGERS
+22 -63
View File
@@ -2,7 +2,8 @@ from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import time, uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID, CONF_TRIGGER_ID
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID
from esphome.cpp_generator import MockObj
DEPENDENCIES = ["uart"]
DOMAIN = "tuya"
@@ -17,6 +18,8 @@ tuya_ns = cg.esphome_ns.namespace("tuya")
TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True)
Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice)
TuyaDatapoint = tuya_ns.struct("TuyaDatapoint")
DPTYPE_ANY = "any"
DPTYPE_RAW = "raw"
DPTYPE_BOOL = "bool"
@@ -26,60 +29,18 @@ DPTYPE_STRING = "string"
DPTYPE_ENUM = "enum"
DPTYPE_BITMASK = "bitmask"
# Automation argument type, plus the expected TuyaDatapointType and the field forwarded
DATAPOINT_TYPES = {
DPTYPE_ANY: tuya_ns.struct("TuyaDatapoint"),
DPTYPE_RAW: cg.std_vector.template(cg.uint8),
DPTYPE_BOOL: cg.bool_,
DPTYPE_INT: cg.int_,
DPTYPE_UINT: cg.uint32,
DPTYPE_STRING: cg.std_string,
DPTYPE_ENUM: cg.uint8,
DPTYPE_BITMASK: cg.uint32,
DPTYPE_ANY: (TuyaDatapoint, None, None),
DPTYPE_RAW: (cg.std_vector.template(cg.uint8), TuyaDatapointType.RAW, "value_raw"),
DPTYPE_BOOL: (cg.bool_, TuyaDatapointType.BOOLEAN, "value_bool"),
DPTYPE_INT: (cg.int_, TuyaDatapointType.INTEGER, "value_int"),
DPTYPE_UINT: (cg.uint32, TuyaDatapointType.INTEGER, "value_uint"),
DPTYPE_STRING: (cg.std_string, TuyaDatapointType.STRING, "value_string"),
DPTYPE_ENUM: (cg.uint8, TuyaDatapointType.ENUM, "value_enum"),
DPTYPE_BITMASK: (cg.uint32, TuyaDatapointType.BITMASK, "value_bitmask"),
}
DATAPOINT_TRIGGERS = {
DPTYPE_ANY: tuya_ns.class_(
"TuyaDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ANY]),
),
DPTYPE_RAW: tuya_ns.class_(
"TuyaRawDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_RAW]),
),
DPTYPE_BOOL: tuya_ns.class_(
"TuyaBoolDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BOOL]),
),
DPTYPE_INT: tuya_ns.class_(
"TuyaIntDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_INT]),
),
DPTYPE_UINT: tuya_ns.class_(
"TuyaUIntDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_UINT]),
),
DPTYPE_STRING: tuya_ns.class_(
"TuyaStringDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_STRING]),
),
DPTYPE_ENUM: tuya_ns.class_(
"TuyaEnumDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ENUM]),
),
DPTYPE_BITMASK: tuya_ns.class_(
"TuyaBitmaskDatapointUpdateTrigger",
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BITMASK]),
),
}
def assign_declare_id(value):
value = value.copy()
value[CONF_TRIGGER_ID] = cv.declare_id(
DATAPOINT_TRIGGERS[value[CONF_DATAPOINT_TYPE]]
)(value[CONF_TRIGGER_ID].id)
return value
CONF_TUYA_ID = "tuya_id"
CONFIG_SCHEMA = (
@@ -93,15 +54,11 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DATAPOINT_TRIGGERS[DPTYPE_ANY]
),
cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_DATAPOINT_TYPE, default=DPTYPE_ANY): cv.one_of(
*DATAPOINT_TRIGGERS, lower=True
*DATAPOINT_TYPES, lower=True
),
},
extra_validators=assign_declare_id,
}
),
}
)
@@ -124,9 +81,11 @@ async def to_code(config):
for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]:
cg.add(var.add_ignore_mcu_update_on_datapoints(dp))
for conf in config.get(CONF_ON_DATAPOINT_UPDATE, []):
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, conf[CONF_SENSOR_DATAPOINT]
)
await automation.build_automation(
trigger, [(DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]], "x")], conf
type_, expected, field = DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]]
forward = None
if expected is not None:
forward = [getattr(MockObj("x", ".").expect_type(expected), field)]
callback = await automation.build_trigger_callback(
[(type_, "x")], conf, params=[(TuyaDatapoint, "x")], forward=forward
)
cg.add(var.register_listener(conf[CONF_SENSOR_DATAPOINT], callback))
-65
View File
@@ -1,65 +0,0 @@
#include "esphome/core/log.h"
#include "automation.h"
ESPHOME_LOG_TAG(TAG, "tuya.automation");
namespace esphome::tuya {
void check_expected_datapoint(const TuyaDatapoint &dp, TuyaDatapointType expected) {
if (dp.type != expected) {
ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", dp.id,
static_cast<uint8_t>(expected), static_cast<uint8_t>(dp.type));
}
}
TuyaRawDatapointUpdateTrigger::TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::RAW);
this->trigger(dp.value_raw);
});
}
TuyaBoolDatapointUpdateTrigger::TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::BOOLEAN);
this->trigger(dp.value_bool);
});
}
TuyaIntDatapointUpdateTrigger::TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::INTEGER);
this->trigger(dp.value_int);
});
}
TuyaUIntDatapointUpdateTrigger::TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::INTEGER);
this->trigger(dp.value_uint);
});
}
TuyaStringDatapointUpdateTrigger::TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::STRING);
this->trigger(dp.value_string);
});
}
TuyaEnumDatapointUpdateTrigger::TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::ENUM);
this->trigger(dp.value_enum);
});
}
TuyaBitmaskDatapointUpdateTrigger::TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
check_expected_datapoint(dp, TuyaDatapointType::BITMASK);
this->trigger(dp.value_bitmask);
});
}
} // namespace esphome::tuya
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "tuya.h"
#include <vector>
namespace esphome::tuya {
class TuyaDatapointUpdateTrigger final : public Trigger<TuyaDatapoint> {
public:
explicit TuyaDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { this->trigger(dp); });
}
};
class TuyaRawDatapointUpdateTrigger final : public Trigger<std::vector<uint8_t>> {
public:
explicit TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaBoolDatapointUpdateTrigger final : public Trigger<bool> {
public:
explicit TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaIntDatapointUpdateTrigger final : public Trigger<int> {
public:
explicit TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaUIntDatapointUpdateTrigger final : public Trigger<uint32_t> {
public:
explicit TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaStringDatapointUpdateTrigger final : public Trigger<std::string> {
public:
explicit TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaEnumDatapointUpdateTrigger final : public Trigger<uint8_t> {
public:
explicit TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaBitmaskDatapointUpdateTrigger final : public Trigger<uint32_t> {
public:
explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
} // namespace esphome::tuya
+13 -6
View File
@@ -482,7 +482,7 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) {
// Run through listeners
for (auto &listener : this->listeners_) {
if (listener.datapoint_id == datapoint.id)
listener.on_datapoint(datapoint);
listener.on_datapoint.call(datapoint);
}
}
}
@@ -796,20 +796,27 @@ void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datap
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_DELIVER, .payload = buffer});
}
void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) {
auto listener = TuyaDatapointListener{
void Tuya::register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> func) {
this->listeners_.push_back(TuyaDatapointListener{
.datapoint_id = datapoint_id,
.on_datapoint = func,
};
this->listeners_.push_back(listener);
});
// Run through existing datapoints
for (auto &datapoint : this->datapoints_) {
if (datapoint.id == datapoint_id)
func(datapoint);
func.call(datapoint);
}
}
const TuyaDatapoint &TuyaDatapoint::expect_type(TuyaDatapointType expected) const {
if (this->type != expected) {
ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", this->id,
static_cast<uint8_t>(expected), static_cast<uint8_t>(this->type));
}
return *this;
}
TuyaInitState Tuya::get_init_state() { return this->init_state_; }
} // namespace esphome::tuya
+8 -2
View File
@@ -37,11 +37,14 @@ struct TuyaDatapoint {
};
std::string value_string;
std::vector<uint8_t> value_raw;
/// Log a warning when this datapoint is not of type `expected`, and return it.
const TuyaDatapoint &expect_type(TuyaDatapointType expected) const;
};
struct TuyaDatapointListener {
uint8_t datapoint_id;
std::function<void(TuyaDatapoint)> on_datapoint;
Callback<void(const TuyaDatapoint &)> on_datapoint;
};
enum class TuyaCommandType : uint8_t {
@@ -91,7 +94,9 @@ class Tuya final : public Component, public uart::UARTDevice {
void setup() override;
void loop() override;
void dump_config() override;
void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
template<typename F> void register_listener(uint8_t datapoint_id, F &&func) {
this->register_listener_(datapoint_id, Callback<void(const TuyaDatapoint &)>::create(std::forward<F>(func)));
}
void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value);
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value);
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value);
@@ -117,6 +122,7 @@ class Tuya final : public Component, public uart::UARTDevice {
}
protected:
void register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> func);
void handle_char_(uint8_t c);
void handle_datapoints_(const uint8_t *buffer, size_t len);
optional<TuyaDatapoint> get_datapoint_(uint8_t datapoint_id);
+19
View File
@@ -533,6 +533,25 @@ def final_validate_device_schema(
)
def subtree_references_uart(
node: object, uart_id: str, conf_key: str = CONF_UART_ID
) -> bool:
"""Return True if any dict in the subtree has a conf_key entry naming this bus.
For the final validation of a component that needs a UART for itself. Bare
`id:` references (a uart.write action) and lambdas are not found.
"""
if isinstance(node, dict):
return any(
(key == conf_key and str(value) == uart_id)
or subtree_references_uart(value, uart_id, conf_key)
for key, value in node.items()
)
if isinstance(node, list):
return any(subtree_references_uart(item, uart_id, conf_key) for item in node)
return False
async def register_uart_device(var, config):
"""Register a UART device, setting up all the internal values.
@@ -1,9 +1,15 @@
#include "uart_component.h"
#include "esphome/core/application.h"
#include <algorithm>
namespace esphome::uart {
ESPHOME_LOG_TAG(TAG, "uart");
// Keeps the pacing product in 32 bits up to about 10 Mbaud.
static constexpr uint32_t MAX_PACE_SPAN_MS = 4000;
bool UARTComponent::check_read_timeout_(size_t len) {
if (this->available() >= len)
return true;
@@ -19,6 +25,19 @@ bool UARTComponent::check_read_timeout_(size_t len) {
return true;
}
size_t UARTComponent::paced_write_room(uint32_t last_write_ms) {
size_t room = this->available_for_write();
if (room != SIZE_MAX) {
return room;
}
// A pass woken early writes little.
uint32_t span =
std::min({App.get_loop_component_start_time() - last_write_ms, App.get_loop_interval(), MAX_PACE_SPAN_MS});
// 10 bits per byte on the line.
uint32_t paced = this->baud_rate_ / 10 * span / 1000;
return std::max<size_t>(1, paced);
}
void UARTComponent::set_rx_full_threshold_ms(uint8_t time) {
uint8_t bytelength = this->data_bits_ + this->stop_bits_ + 1;
if (this->parity_ != UARTParityOptions::UART_CONFIG_PARITY_NONE)
+4
View File
@@ -86,6 +86,10 @@ class UARTComponent {
// Platforms that cannot tell return SIZE_MAX: write_array() takes everything and may block.
virtual size_t available_for_write() { return SIZE_MAX; }
// Bytes a write can take now. Where available_for_write() cannot tell (SIZE_MAX), paces to the line time since
// last_write_ms, a loop start time: at most one loop interval and 4 s, at least one byte.
size_t paced_write_room(uint32_t last_write_ms);
// Pure virtual method to block until all bytes have been written to the UART bus.
// @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful.
virtual UARTFlushResult flush() = 0;
+1 -13
View File
@@ -71,18 +71,6 @@ CONFIG_SCHEMA = cv.All(
)
def _subtree_references_uart(node: object, uart_id: str) -> bool:
if isinstance(node, dict):
return any(
(key == CONF_UART_ID and str(value) == uart_id)
or _subtree_references_uart(value, uart_id)
for key, value in node.items()
)
if isinstance(node, list):
return any(_subtree_references_uart(item, uart_id) for item in node)
return False
def _reject_dummy_receiver(uart_conf: ConfigType) -> ConfigType:
debug = uart_conf.get(CONF_DEBUG)
if isinstance(debug, dict) and debug.get(CONF_DUMMY_RECEIVER):
@@ -111,7 +99,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
# Bare `id:` references (a uart.write action) and lambdas are not caught.
if not CORE.testing_mode:
for domain, domain_conf in full_config.items():
if domain != DOMAIN and _subtree_references_uart(domain_conf, uart_id):
if domain != DOMAIN and uart.subtree_references_uart(domain_conf, uart_id):
raise cv.Invalid(
f"The UART '{uart_id}' is also used by '{domain}'. "
"uart_tcp requires exclusive use of that UART.",
+1 -13
View File
@@ -11,9 +11,6 @@ namespace esphome::uart_tcp {
ESPHOME_LOG_TAG(TAG, "uart_tcp");
// Keeps the pacing product in 32 bits up to about 10 Mbaud.
static constexpr uint32_t MAX_PACE_SPAN_MS = 4000;
void UartTcp::setup() {
this->link_.begin(TAG);
#ifdef USE_SOCKET_TCP_LISTENER
@@ -75,16 +72,7 @@ void UartTcp::sync_link_() {
void UartTcp::read_socket_() {
// A hardware write blocks until the driver takes every byte. Leave what does
// not fit in the socket, so TCP flow control throttles the peer.
size_t room = this->parent_->available_for_write();
if (room == SIZE_MAX) {
// Capacity unknown on this platform; pace to the UART time since the last write,
// at most one loop interval and 4 s, so a pass woken early by the socket writes little.
uint32_t span = std::min(
{App.get_loop_component_start_time() - this->last_write_ms_, App.get_loop_interval(), MAX_PACE_SPAN_MS});
// 10 bits per byte on the line.
uint32_t paced = this->parent_->get_baud_rate() / 10 * span / 1000;
room = std::max<size_t>(1, paced);
}
size_t room = this->parent_->paced_write_room(this->last_write_ms_);
if (room == 0) {
this->rx_pending_ = true;
return;
+13 -26
View File
@@ -15,7 +15,6 @@ from esphome.const import (
CONF_POSITION_STATE_TOPIC,
CONF_STATE,
CONF_STOP,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GAS,
@@ -62,17 +61,18 @@ VALVE_OPERATIONS = {
}
validate_valve_operation = cv.enum(VALVE_OPERATIONS, upper=True)
# Actions
ValvePublishAction = valve_ns.class_("ValvePublishAction", automation.Action)
# Triggers
ValveOpenTrigger = valve_ns.class_("ValveOpenTrigger", automation.Trigger.template())
ValveClosedTrigger = valve_ns.class_(
"ValveClosedTrigger", automation.Trigger.template()
)
CONF_ON_CLOSED = "on_closed"
# The state callback carries nothing; each automation fires only in its end state.
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_OPEN, "add_on_state_callback", when="{parent}->is_fully_open()"
),
automation.CallbackAutomation(
CONF_ON_CLOSED, "add_on_state_callback", when="{parent}->is_fully_closed()"
),
)
_VALVE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
@@ -89,16 +89,8 @@ _VALVE_SCHEMA = (
cv.Optional(CONF_POSITION_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic
),
cv.Optional(CONF_ON_OPEN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValveOpenTrigger),
}
),
cv.Optional(CONF_ON_CLOSED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValveClosedTrigger),
}
),
cv.Optional(CONF_ON_OPEN): automation.validate_automation({}),
cv.Optional(CONF_ON_CLOSED): automation.validate_automation({}),
}
)
)
@@ -134,12 +126,7 @@ def valve_schema(
async def _setup_valve_core(var, config):
setup_device_class(config)
for conf in config.get(CONF_ON_OPEN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_CLOSED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if mqtt_id_config := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id_config, var)
-37
View File
@@ -1,37 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "valve.h"
namespace esphome::valve {
class ValveOpenTrigger final : public Trigger<> {
public:
ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) {
a_valve->add_on_state_callback([this]() {
if (this->valve_->is_fully_open()) {
this->trigger();
}
});
}
protected:
Valve *valve_;
};
class ValveClosedTrigger final : public Trigger<> {
public:
ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) {
a_valve->add_on_state_callback([this]() {
if (this->valve_->is_fully_closed()) {
this->trigger();
}
});
}
protected:
Valve *valve_;
};
} // namespace esphome::valve
@@ -4,7 +4,7 @@
namespace esphome::waveshare_epaper {
static const char *const TAG = "waveshare_2.13v3";
ESPHOME_LOG_TAG(TAG, "waveshare_2.13v3");
static const uint8_t PARTIAL_LUT[] = {
0x32, // cmd
@@ -7,7 +7,7 @@
namespace esphome::waveshare_epaper {
static const char *const TAG = "waveshare_epaper";
ESPHOME_LOG_TAG(TAG, "waveshare_epaper");
static const uint8_t LUT_SIZE_WAVESHARE = 30;
+2 -1
View File
@@ -1,5 +1,6 @@
#ifdef USE_ZEPHYR
#include "gpio.h"
#include "esphome/core/hal.h"
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/util.h>
#include "esphome/core/log.h"
@@ -79,7 +80,7 @@ ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const {
return ISRInternalGPIOPin((void *) arg);
}
void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
void ZephyrGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
if (!device_is_ready(this->gpio_)) {
ESP_LOGE(TAG, "Cannot attach interrupt: GPIO device not ready");
return;
+12 -9
View File
@@ -1,7 +1,7 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/hal.h"
#include "esphome/core/gpio_pin.h"
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
namespace esphome::zephyr {
@@ -16,7 +16,7 @@ struct ZephyrGPIOInterrupt {
void *arg{nullptr};
};
class ZephyrGPIOPin final : public InternalGPIOPin {
class ZephyrGPIOPin final : public GPIOPin {
public:
ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) {
this->gpio_ = gpio;
@@ -32,14 +32,18 @@ class ZephyrGPIOPin final : public InternalGPIOPin {
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return this->pin_; }
bool is_inverted() const override { return this->inverted_; }
void detach_interrupt() const;
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
ISRInternalGPIOPin to_isr() const;
uint8_t get_pin() const { return this->pin_; }
bool is_inverted() const { return this->inverted_; }
bool is_internal() override { return true; }
gpio::Flags get_flags() const override { return flags_; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
const device *gpio_{nullptr};
const char *pin_name_prefix_{nullptr};
gpio::Flags flags_{};
@@ -48,8 +52,7 @@ class ZephyrGPIOPin final : public InternalGPIOPin {
bool inverted_{};
bool value_{false};
// attach_interrupt()/detach_interrupt() are const (matching the base class), so
// the interrupt state they manage has to be mutable.
// attach_interrupt_()/detach_interrupt() are const, so their interrupt state is mutable.
mutable ZephyrGPIOInterrupt interrupt_{};
};
+23 -5
View File
@@ -292,14 +292,17 @@ void Component::status_set_warning(const LogString *message) {
}
void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); }
void Component::status_set_error(const LogString *message) {
if (!this->set_status_flag_(STATUS_LED_ERROR))
return;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
if (this->set_error_flag_(message) && message != nullptr) {
store_component_error_message(this, message);
}
}
bool Component::set_error_flag_(const LogString *message) {
if (!this->set_status_flag_(STATUS_LED_ERROR))
return false;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
return true;
}
void Component::status_clear_warning_slow_path_() {
this->component_state_ &= ~STATUS_LED_WARNING;
// Clear the app-wide STATUS_LED_WARNING bit only if setup has finished
@@ -329,6 +332,21 @@ void Component::status_momentary_error(const char *name, uint32_t length) {
this->status_set_error();
this->set_timeout(name, length, [this]() { this->status_clear_error(); });
}
void Component::status_momentary_warning(uint32_t length) {
this->status_momentary_warning(static_cast<const LogString *>(nullptr), length);
}
void Component::status_momentary_warning(const LogString *message, uint32_t length) {
this->status_set_warning(message);
this->set_timeout(InternalSchedulerID::STATUS_WARNING, length, [this]() { this->status_clear_warning(); });
}
void Component::status_momentary_error(uint32_t length) {
this->status_momentary_error(static_cast<const LogString *>(nullptr), length);
}
void Component::status_momentary_error(const LogString *message, uint32_t length) {
// Not stored: a cleared momentary error must not show up later as a FAILED reason
this->set_error_flag_(message);
this->set_timeout(InternalSchedulerID::STATUS_ERROR, length, [this]() { this->status_clear_error(); });
}
void Component::dump_config() {}
// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size
+14
View File
@@ -67,6 +67,8 @@ inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
/// with component-level NUMERIC_ID values, even if the uint32_t values overlap.
enum class InternalSchedulerID : uint32_t {
POLLING_UPDATE = 0, // PollingComponent interval
STATUS_WARNING = 1, // status_momentary_warning() without a name
STATUS_ERROR = 2, // status_momentary_error() without a name
};
// Forward declaration
@@ -307,6 +309,11 @@ class Component {
*/
void status_momentary_warning(const char *name, uint32_t length = 5000);
/// Set warning status flag and clear it after `length` ms. A new call restarts the timeout.
/// `message` is logged in place of "unspecified" when the flag gets set.
void status_momentary_warning(uint32_t length = 5000);
void status_momentary_warning(const LogString *message, uint32_t length = 5000);
/** Set error status flag and automatically clear it after a timeout.
*
* @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name).
@@ -316,6 +323,11 @@ class Component {
*/
void status_momentary_error(const char *name, uint32_t length = 5000);
/// Set error status flag and clear it after `length` ms. A new call restarts the timeout.
/// `message` is logged in place of "unspecified"; unlike status_set_error() it is not stored.
void status_momentary_error(uint32_t length = 5000);
void status_momentary_error(const LogString *message, uint32_t length = 5000);
bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; }
/** Get the integration where this component was declared as a LogString for logging.
@@ -356,6 +368,8 @@ class Component {
/// Note: Callers often use the return value to decide whether to log a warning/error,
/// so once a flag is set, subsequent (potentially different) messages may be suppressed.
bool set_status_flag_(uint8_t flag);
/// Set the error flag and log it; true when it was not set before.
bool set_error_flag_(const LogString *message);
/** Set an interval function with a const char* name. Empty name means no cancelling possible.
*
-1
View File
@@ -365,7 +365,6 @@
#define USE_ESP32_CRASH_HANDLER
#define USE_ESP32_VASPRINTF_STUB
#define USE_ESP32_SSCANF_STUB
#define USE_ESP32_INTERNAL_GPIO
#define USE_MQTT_IDF_ENQUEUE
#define USE_ESPHOME_TASK_LOG_BUFFER
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
+39 -145
View File
@@ -1,150 +1,44 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include "esphome/core/gpio_pin.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Each platform has exactly one internal pin class, so InternalGPIOPin is an alias for
// it instead of an abstract base class: calls through the alias are direct and can be inlined.
#if defined(USE_ESP32)
#include "esphome/components/esp32/gpio.h"
namespace esphome {
using InternalGPIOPin = esp32::ESP32InternalGPIOPin;
}
#elif defined(USE_ESP8266)
#include "esphome/components/esp8266/gpio.h"
namespace esphome {
using InternalGPIOPin = esp8266::ESP8266GPIOPin;
}
#elif defined(USE_LIBRETINY)
#include "esphome/components/libretiny/gpio_arduino.h"
namespace esphome {
using InternalGPIOPin = libretiny::ArduinoInternalGPIOPin;
}
#elif defined(USE_RP2)
#include "esphome/components/rp2/gpio.h"
namespace esphome {
using InternalGPIOPin = rp2::RP2GPIOPin;
}
#elif defined(USE_HOST)
#include "esphome/components/host/gpio.h"
namespace esphome {
using InternalGPIOPin = host::HostGPIOPin;
}
#elif defined(USE_ZEPHYR)
#include "esphome/components/zephyr/gpio.h"
namespace esphome {
using InternalGPIOPin = zephyr::ZephyrGPIOPin;
}
#else
#error "gpio.h: no internal pin class for this platform"
#endif
namespace esphome {
/// Maximum buffer size for dump_summary output
inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48;
#ifdef USE_ESP8266
#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin)
#else
#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin)
#endif
// put GPIO flags in a namespace to not pollute esphome namespace
namespace gpio {
enum Flags : uint8_t {
// Can't name these just INPUT because of Arduino defines :(
FLAG_NONE = 0x00,
FLAG_INPUT = 0x01,
FLAG_OUTPUT = 0x02,
FLAG_OPEN_DRAIN = 0x04,
FLAG_PULLUP = 0x08,
FLAG_PULLDOWN = 0x10,
FLAG_HOLD = 0x20,
};
class FlagsHelper {
public:
constexpr FlagsHelper(Flags val) : val_(val) {}
constexpr operator Flags() const { return val_; }
protected:
Flags val_;
};
constexpr FlagsHelper operator&(Flags lhs, Flags rhs) {
return static_cast<Flags>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr FlagsHelper operator|(Flags lhs, Flags rhs) {
return static_cast<Flags>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
enum InterruptType : uint8_t {
INTERRUPT_RISING_EDGE = 1,
INTERRUPT_FALLING_EDGE = 2,
INTERRUPT_ANY_EDGE = 3,
INTERRUPT_LOW_LEVEL = 4,
INTERRUPT_HIGH_LEVEL = 5,
};
} // namespace gpio
class GPIOPin {
public:
virtual void setup() = 0;
virtual void pin_mode(gpio::Flags flags) = 0;
/**
* @brief Retrieve GPIO pin flags.
*
* @return The GPIO flags describing the pin mode and properties.
*/
virtual gpio::Flags get_flags() const = 0;
virtual bool digital_read() = 0;
virtual void digital_write(bool value) = 0;
/// Write a summary of this pin to the provided buffer.
/// @param buffer The buffer to write to
/// @param len The size of the buffer (must be > 0)
/// @return The number of characters that would be written (excluding null terminator),
/// which may exceed len-1 if truncation occurred (snprintf semantics)
virtual size_t dump_summary(char *buffer, size_t len) const;
virtual bool is_internal() { return false; }
#ifdef USE_GPIO_HOLD
bool get_hold() const { return this->get_flags() & gpio::FLAG_HOLD; }
virtual bool is_held() const { return false; }
#else
bool get_hold() const { return false; }
bool is_held() const { return false; }
#endif
};
/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions)
class ISRInternalGPIOPin {
public:
ISRInternalGPIOPin() = default;
ISRInternalGPIOPin(void *arg) : arg_(arg) {}
bool digital_read();
void digital_write(bool value);
void clear_interrupt();
void pin_mode(gpio::Flags flags);
protected:
void *arg_{nullptr};
};
class InternalGPIOPin : public GPIOPin {
public:
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
this->attach_interrupt(reinterpret_cast<void (*)(void *)>(func), arg, type);
}
virtual void detach_interrupt() const = 0;
virtual ISRInternalGPIOPin to_isr() const = 0;
virtual uint8_t get_pin() const = 0;
bool is_internal() override { return true; }
virtual bool is_inverted() const = 0;
protected:
virtual void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const = 0;
};
// Inline default implementation for GPIOPin::dump_summary.
// Writes an empty summary; subclasses override to provide pin details.
inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const {
if (len > 0)
buffer[0] = '\0';
return 0;
}
// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp
inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) {
char buffer[GPIO_SUMMARY_MAX_LEN];
size_t len = pin->dump_summary(buffer, sizeof(buffer));
len = std::min(len, sizeof(buffer) - 1);
esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer);
}
// log_pin function declarations - implementation in gpio.cpp
#ifdef USE_ESP8266
void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin);
#else
void log_pin(const char *tag, const char *prefix, GPIOPin *pin);
#endif
static_assert(InternalGPIOPinContract<InternalGPIOPin>,
"The platform's internal pin class is missing part of the InternalGPIOPin surface "
"(esphome/core/gpio_pin.h)");
} // namespace esphome
+147
View File
@@ -0,0 +1,147 @@
#pragma once
#include <algorithm>
#include <concepts>
#include <cstdint>
#include <type_traits>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
/// Maximum buffer size for dump_summary output
inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48;
#ifdef USE_ESP8266
#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin)
#else
#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin)
#endif
// put GPIO flags in a namespace to not pollute esphome namespace
namespace gpio {
enum Flags : uint8_t {
// Can't name these just INPUT because of Arduino defines :(
FLAG_NONE = 0x00,
FLAG_INPUT = 0x01,
FLAG_OUTPUT = 0x02,
FLAG_OPEN_DRAIN = 0x04,
FLAG_PULLUP = 0x08,
FLAG_PULLDOWN = 0x10,
FLAG_HOLD = 0x20,
};
class FlagsHelper {
public:
constexpr FlagsHelper(Flags val) : val_(val) {}
constexpr operator Flags() const { return val_; }
protected:
Flags val_;
};
constexpr FlagsHelper operator&(Flags lhs, Flags rhs) {
return static_cast<Flags>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr FlagsHelper operator|(Flags lhs, Flags rhs) {
return static_cast<Flags>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
enum InterruptType : uint8_t {
INTERRUPT_RISING_EDGE = 1,
INTERRUPT_FALLING_EDGE = 2,
INTERRUPT_ANY_EDGE = 3,
INTERRUPT_LOW_LEVEL = 4,
INTERRUPT_HIGH_LEVEL = 5,
};
} // namespace gpio
class GPIOPin {
public:
virtual void setup() = 0;
virtual void pin_mode(gpio::Flags flags) = 0;
/**
* @brief Retrieve GPIO pin flags.
*
* @return The GPIO flags describing the pin mode and properties.
*/
virtual gpio::Flags get_flags() const = 0;
virtual bool digital_read() = 0;
virtual void digital_write(bool value) = 0;
/// Write a summary of this pin to the provided buffer.
/// @param buffer The buffer to write to
/// @param len The size of the buffer (must be > 0)
/// @return The number of characters that would be written (excluding null terminator),
/// which may exceed len-1 if truncation occurred (snprintf semantics)
virtual size_t dump_summary(char *buffer, size_t len) const;
/// Only the platform pin class behind the InternalGPIOPin alias may return true:
/// callers static_cast to InternalGPIOPin on it.
virtual bool is_internal() { return false; }
#ifdef USE_GPIO_HOLD
bool get_hold() const { return this->get_flags() & gpio::FLAG_HOLD; }
virtual bool is_held() const { return false; }
#else
bool get_hold() const { return false; }
bool is_held() const { return false; }
#endif
};
/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions)
class ISRInternalGPIOPin {
public:
ISRInternalGPIOPin() = default;
ISRInternalGPIOPin(void *arg) : arg_(arg) {}
bool digital_read();
void digital_write(bool value);
void clear_interrupt();
void pin_mode(gpio::Flags flags);
protected:
void *arg_{nullptr};
};
// What callers of the InternalGPIOPin alias (bound in esphome/core/gpio.h) rely on beyond
// GPIOPin. The class must be final so calls to the GPIOPin virtuals through the alias are direct.
// int is arbitrary: any T * exercises the public attach_interrupt<T> template.
template<typename T>
concept InternalGPIOPinContract = std::derived_from<T, GPIOPin> && std::is_final_v<T> &&
requires(const T pin, void (*func)(int *), int *arg, gpio::InterruptType type) {
{ pin.get_pin() } -> std::same_as<uint8_t>;
{ pin.is_inverted() } -> std::same_as<bool>;
{ pin.to_isr() } -> std::same_as<ISRInternalGPIOPin>;
{ pin.detach_interrupt() } -> std::same_as<void>;
{ pin.attach_interrupt(func, arg, type) } -> std::same_as<void>;
};
// Inline default implementation for GPIOPin::dump_summary.
// Writes an empty summary; subclasses override to provide pin details.
inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const {
if (len > 0)
buffer[0] = '\0';
return 0;
}
// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp
inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) {
char buffer[GPIO_SUMMARY_MAX_LEN];
size_t len = pin->dump_summary(buffer, sizeof(buffer));
len = std::min(len, sizeof(buffer) - 1);
esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer);
}
// log_pin function declarations - implementation in gpio.cpp
#ifdef USE_ESP8266
void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin);
#else
void log_pin(const char *tag, const char *prefix, GPIOPin *pin);
#endif
} // namespace esphome
+8
View File
@@ -4,9 +4,17 @@
#include <string>
#include "gpio.h"
#include "esphome/core/defines.h"
#include "esphome/core/time_64.h"
#include "esphome/core/time_conversion.h"
/// Address tables in ESP8266 flash must be constant-initialized: a runtime initializer would write to flash.
#if defined(USE_ESP8266) && defined(__GNUC__) && !defined(__clang__)
#define ESPHOME_FLASH_CONSTINIT constinit
#else
#define ESPHOME_FLASH_CONSTINIT
#endif
// Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(),
// inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem
// helpers) live next to each platform component as components/<platform>/hal.h
+41 -8
View File
@@ -10,6 +10,7 @@ from esphome.core import (
ID,
Define,
EnumValue,
EsphomeError,
HexInt,
Lambda,
Library,
@@ -447,13 +448,16 @@ class LineComment(Statement):
class ProgmemAssignmentExpression(AssignmentExpression):
__slots__ = ()
__slots__ = ("constexpr",)
def __init__(self, type_, name, rhs):
def __init__(self, type_, name, rhs, constexpr: bool = True):
super().__init__(type_, "", name, rhs)
self.constexpr = constexpr
def __str__(self):
return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}"
if self.constexpr:
return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}"
return f"ESPHOME_FLASH_CONSTINIT static {self.type} const {self.name}[] PROGMEM = {self.rhs}"
class StaticConstAssignmentExpression(AssignmentExpression):
@@ -493,20 +497,34 @@ def extern_progmem_array(
def shared_progmem_array(
name: str, type_: "MockObjClass", rhs: SafeExpType, *, share: bool = True
name: str,
type_: "MockObjClass",
rhs: SafeExpType,
*,
share: bool = True,
constexpr: bool = True,
) -> "MockObj":
"""Emit a global PROGMEM array once per distinct type and contents; later calls reuse it.
The array is ``static constexpr``, so elements must be constant expressions and lambdas
must be captureless. Its name is made unique against every config id and variable.
The array is ``static constexpr`` by default, so elements must be constant expressions and
lambdas must be captureless. Its name is made unique against every config id and variable.
``share=False`` always emits a new array, e.g. for lambdas that may keep static state.
``constexpr=False`` is for tables of generated object pointers: each top level variable
element must pass ``is_static_pointer`` or ``EsphomeError`` is raised.
"""
from esphome.config import iter_ids
from esphome.config_validation import RESERVED_IDS
arrays: dict[str, MockObj] = CORE.data.setdefault("shared_progmem_array", {})
rhs = safe_exp(rhs)
key = f"{type_} {rhs}"
if not constexpr and isinstance(rhs, ArrayInitializer):
for arg in rhs.args:
if isinstance(arg, MockObj) and not is_static_pointer(arg):
raise EsphomeError(
f"'{arg}' must be created with cg.new_Pvariable so its address is known "
"at compile time"
)
key = f"{type_} {rhs} {constexpr}"
if share and (array := arrays.get(key)) is not None:
return array
used = {str(i) for i, _ in iter_ids(CORE.config)}
@@ -514,7 +532,7 @@ def shared_progmem_array(
used |= set(RESERVED_IDS) | CORE.loaded_integrations
id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_)
# Global, so any scope can use it; anything a lambda references is already declared.
CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs))
CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs, constexpr))
array = MockObj(id_, ".")
CORE.register_variable(id_, array)
if share:
@@ -703,6 +721,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
)
placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args)
CORE.add(ExpressionStatement(placement_new))
CORE.data.setdefault(_STATIC_POINTER_IDS, set()).add(id_.id)
else:
decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
CORE.add_global(decl)
@@ -712,6 +731,20 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
return obj
_STATIC_POINTER_IDS = "static_pointer_ids"
def is_static_pointer(obj: SafeExpType) -> bool:
"""True if ``obj`` is a Pvariable whose object was placement constructed in static storage.
Its pointer is then an address known at compile time and may appear in a ``constexpr=False``
PROGMEM table; a pointer assigned in ``setup()`` may not.
"""
return isinstance(obj, MockObj) and str(obj.base) in CORE.data.get(
_STATIC_POINTER_IDS, ()
)
def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
"""Declare a new pointer variable in the code generation by calling it's constructor
with the given arguments.
-6
View File
@@ -1273,12 +1273,6 @@ def lint_no_std_nothrow(fname, match):
r"\s*(?:=\s*\{?|[{(])\s*\"",
prefilter="TAG",
include=["esphome/components/*.cpp", "esphome/components/**/*.cpp"],
# Deprecated components, converted in a follow-up PR
exclude=[
"esphome/components/st7735/*",
"esphome/components/st7789v/*",
"esphome/components/waveshare_epaper/*",
],
)
def lint_log_tag_macro(fname, match):
return (
@@ -3,6 +3,8 @@
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/light/color_mode.h"
#include "esphome/components/light/light_effect.h"
#include "esphome/components/light/light_effect_names.h"
namespace esphome::api::benchmarks {
@@ -150,7 +152,17 @@ BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse);
// --- ListEntitiesLightResponse ---
static light::ColorModeMask light_color_modes;
static FixedVector<const char *> light_effects;
class BenchEffect : public light::LightEffect {
public:
explicit BenchEffect(const char *name) : LightEffect(name) {}
void apply() override {}
};
static BenchEffect rainbow_effect("Rainbow");
static BenchEffect strobe_effect("Strobe");
static light::LightEffect *const LIGHT_EFFECT_TABLE[] = {&rainbow_effect, &strobe_effect};
static const ConstVector<light::LightEffect *> light_effect_list(LIGHT_EFFECT_TABLE, 2);
static const light::LightEffectNames light_effects(light_effect_list);
static ListEntitiesLightResponse make_light_response() {
// Initialize static data on first call
@@ -158,10 +170,6 @@ static ListEntitiesLightResponse make_light_response() {
if (!initialized) {
light_color_modes.insert(light::ColorMode::RGB_WHITE);
light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE);
light_effects.init(3);
light_effects.push_back("None");
light_effects.push_back("Rainbow");
light_effects.push_back("Strobe");
initialized = true;
}
@@ -2,6 +2,14 @@
from collections.abc import Callable
from pathlib import Path
import re
import pytest
import esphome.codegen as cg
from esphome.components import light
from esphome.components.light import types as light_types
from esphome.core import ID, EsphomeError
def test_default_flash_length_and_empty_effects_are_not_emitted(
@@ -16,4 +24,20 @@ def test_default_flash_length_and_empty_effects_are_not_emitted(
assert "bare_light->set_flash_transition_length(" not in main_cpp
assert "bare_light->add_effects(" not in main_cpp
assert "fancy_light->set_flash_transition_length(500);" in main_cpp
assert "fancy_light->add_effects({" in main_cpp
call = re.search(r"fancy_light->add_effects\((\w+), (\d+)\);", main_cpp)
assert call is not None
# The effect pointers form a flash table; they are address constants, not constexpr
assert re.search(
rf"static light::LightEffect \* const {call.group(1)}\[\] PROGMEM = \{{[^}}]+\}};",
main_cpp,
)
def test_effect_assigned_in_setup_is_rejected() -> None:
"""A pointer assigned in setup() would make the flash table need dynamic init."""
dynamic_effect = cg.Pvariable(
ID("dynamic_effect", is_declaration=True, type=light_types.LightEffect),
cg.RawExpression("make_effect()"),
)
with pytest.raises(EsphomeError, match="dynamic_effect"):
light._effects_table([dynamic_effect])
+3 -3
View File
@@ -1,5 +1,5 @@
"""automation.cpp (CronTrigger and SyncTrigger) is only compiled when an
on_time or on_time_sync automation exists, so the define must follow them."""
"""automation.cpp (CronTrigger) is only compiled when an on_time automation
exists, so the define must follow it; on_time_sync needs no class."""
from collections.abc import Callable
from pathlib import Path
@@ -14,7 +14,7 @@ from esphome.core import CORE
[
("no_triggers.yaml", False),
("on_time.yaml", True),
("on_time_sync.yaml", True),
("on_time_sync.yaml", False),
],
)
def test_triggers_define_follows_automations(
@@ -0,0 +1,10 @@
datetime:
- platform: template
name: Datetime Trigger Test
id: datetime_trigger_test
type: datetime
optimistic: true
on_value:
- logger.log:
format: "New value %04d-%02d-%02d"
args: [x.year, x.month, x.day_of_month]
@@ -0,0 +1,3 @@
packages:
datetime: !include common.yaml
triggers: !include common-triggers.yaml
@@ -0,0 +1,4 @@
# No pin anywhere in the config: the pin class methods deep_sleep calls must still link
deep_sleep:
run_duration: 10s
sleep_duration: 50s
@@ -0,0 +1,26 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
model: SSD1306_128X64
reset_pin: ${reset_pin}
id: display_page_change_display
pages:
- id: display_page_change_page1
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
- id: display_page_change_page2
lambda: |-
it.rectangle(0, 0, 10, 10);
on_page_change:
- then:
lambda: 'ESP_LOGD("display", "any change, %p %p", from, to);'
- from: display_page_change_page1
then:
lambda: 'ESP_LOGD("display", "left page 1, %p %p", from, to);'
- to: display_page_change_page1
then:
lambda: 'ESP_LOGD("display", "entered page 1, %p %p", from, to);'
- from: display_page_change_page1
to: display_page_change_page2
then:
lambda: 'ESP_LOGD("display", "1 -> 2, %p %p", from, to);'
@@ -0,0 +1,6 @@
substitutions:
reset_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
display: !include common-page-change.yaml
@@ -0,0 +1,24 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: graphical_display_menu_triggers_display
model: SSD1306_128X64
reset_pin: ${reset_pin}
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
font:
- file: "gfonts://Roboto"
id: graphical_display_menu_triggers_font
size: 20
graphical_display_menu:
id: graphical_display_menu_triggers_menu
display: graphical_display_menu_triggers_display
font: graphical_display_menu_triggers_font
on_redraw:
then:
lambda: 'ESP_LOGI("graphical_display_menu", "redraw, active: %d", it->is_active());'
items:
- type: label
text: "Label"
@@ -0,0 +1,6 @@
substitutions:
reset_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
graphical_display_menu: !include common-triggers.yaml
+2 -1
View File
@@ -32,6 +32,7 @@ esphome:
- lambda: |-
const auto &effects = id(test_monochromatic_light).get_effects();
auto &same = id(test_monochromatic_light).get_effects();
const auto copy = id(test_monochromatic_light).get_effects(); // the view is copyable
uint32_t total = effects.size();
for (auto *effect : effects) {
ESP_LOGD("test", "Effect %s", effect->get_name().c_str());
@@ -39,7 +40,7 @@ esphome:
if (total > 0) {
ESP_LOGD("test", "First %s", effects.at(0)->get_name().c_str());
// Raw pointer iterators on purpose: external code binds std::find's result to `const auto *`
auto *it = std::find(same.begin(), same.end(), effects[0]);
auto *it = std::find(same.begin(), same.end(), copy[0]);
ESP_LOGD("test", "Index %d", (int) (it - same.begin()));
}
@@ -1,6 +1,5 @@
#include <gtest/gtest.h>
#include "esphome/components/light/light_effect.h"
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/light_state.h"
@@ -18,36 +17,8 @@ class BrightnessOutput : public LightOutput {
void write_state(LightState *state) override {}
};
class NoopEffect : public LightEffect {
public:
using LightEffect::LightEffect;
void apply() override {}
};
// start_effect_() is where the uint32_t index is narrowed to the stored uint16_t.
class TestableLightState : public LightState {
public:
using LightState::LightState;
using LightState::start_effect_;
};
} // namespace
// add_effects() is public, so an external component can exceed the codegen cap on effect count;
// an index the uint16_t can't hold must be ignored rather than wrap onto another effect.
TEST(LightStateEffect, IndexAboveUint16IsIgnoredAndKeepsTheActiveEffect) {
BrightnessOutput output;
TestableLightState state(&output);
NoopEffect effect("Noop");
state.add_effects({&effect});
state.start_effect_(1);
ASSERT_EQ(state.get_current_effect_index(), 1u);
state.start_effect_(0x10000u); // unchecked narrowing wraps this to 0, which stops the effect
EXPECT_EQ(state.get_current_effect_index(), 1u);
}
// get_gamma_correct() reads the gamma codegen stores after the lookup table, rounded to two decimals.
TEST(LightStateGamma, ReadsTheGammaStoredWithTheTable) {
static constexpr GammaTable TABLE{{}, 280};
+2
View File
@@ -25,6 +25,8 @@ opentherm:
then:
- lambda: |-
ESP_LOGW("OT", ">> Sending message %d", x.id);
if (x.id == 0)
x.valueHB |= 0x01;
before_process_response:
then:
- lambda: |-
@@ -1,3 +1,4 @@
#ifdef USE_HOST
#include <gtest/gtest.h>
#include "esphome/components/power_supply/power_supply.h"
@@ -6,28 +7,9 @@
namespace esphome::power_supply::testing {
// Minimal dummy internal GPIO pin implementation for testing
class DummyInternalPin : public InternalGPIOPin {
public:
DummyInternalPin() = default;
void setup() override {}
void pin_mode(esphome::gpio::Flags) override {}
esphome::gpio::Flags get_flags() const override { return esphome::gpio::FLAG_NONE; }
bool digital_read() override { return false; }
void digital_write(bool) override {}
void detach_interrupt() const override {}
ISRInternalGPIOPin to_isr() const override { return ISRInternalGPIOPin(); }
uint8_t get_pin() const override { return 0; }
bool is_inverted() const override { return false; }
protected:
// Implement protected attach_interrupt required by InternalGPIOPin
void attach_interrupt(void (*func)(void *), void *arg, esphome::gpio::InterruptType type) const override {}
};
TEST(PowerSupply, HasHigherPriorityThanBusWhenInternalAndEnableOnBoot) {
power_supply::PowerSupply ps;
DummyInternalPin pin;
InternalGPIOPin pin;
ps.set_pin(&pin);
ps.set_enable_on_boot(true);
@@ -37,7 +19,7 @@ TEST(PowerSupply, HasHigherPriorityThanBusWhenInternalAndEnableOnBoot) {
TEST(PowerSupply, FallsBackToIOWhenNotEnableOnBoot) {
power_supply::PowerSupply ps;
DummyInternalPin pin;
InternalGPIOPin pin;
ps.set_pin(&pin);
ps.set_enable_on_boot(false);
@@ -45,3 +27,4 @@ TEST(PowerSupply, FallsBackToIOWhenNotEnableOnBoot) {
}
} // namespace esphome::power_supply::testing
#endif // USE_HOST
@@ -0,0 +1,8 @@
wifi:
ssid: MySSID
password: password1
time:
- platform: sntp
on_time_sync:
- logger.log: Time synchronized
+44 -1
View File
@@ -10,7 +10,50 @@ tuya:
- sensor_datapoint: 6
datapoint_type: raw
then:
- logger.log: Datapoint 6 updated
- logger.log:
format: "Datapoint 6 updated, %u bytes"
args: [x.size()]
- sensor_datapoint: 1
then:
- logger.log:
format: "Datapoint %u updated"
args: [x.id]
- sensor_datapoint: 1
datapoint_type: bool
then:
- logger.log:
format: "Datapoint 1 is %s"
args: ['x ? "on" : "off"']
- sensor_datapoint: 3
datapoint_type: int
then:
- logger.log:
format: "Datapoint 3 is %d"
args: [x]
- sensor_datapoint: 3
datapoint_type: uint
then:
- logger.log:
format: "Datapoint 3 is %u"
args: [x]
- sensor_datapoint: 7
datapoint_type: string
then:
- logger.log:
format: "Datapoint 7 is %s"
args: [x.c_str()]
- sensor_datapoint: 8
datapoint_type: enum
then:
- logger.log:
format: "Datapoint 8 is %u"
args: [x]
- sensor_datapoint: 9
datapoint_type: bitmask
then:
- logger.log:
format: "Datapoint 9 is %u"
args: [x]
binary_sensor:
- platform: tuya
@@ -0,0 +1,62 @@
#include <cstdint>
#include "common.h"
#include "esphome/core/application.h"
namespace esphome::uart::testing {
class RoomUART : public MockUARTComponent {
public:
size_t available_for_write() override { return this->room; }
size_t room{SIZE_MAX};
};
class PacedWriteRoom : public ::testing::Test {
protected:
void SetUp() override {
App.set_loop_interval(16);
this->uart_.set_baud_rate(9600);
}
void TearDown() override {
App.set_loop_interval(16);
this->at_(0);
}
// Publishes now_ms as the loop start time, as Application::loop() does.
void at_(uint32_t now_ms) { LoopBlockingGuard dispatch{nullptr, nullptr, now_ms}; }
RoomUART uart_;
};
TEST_F(PacedWriteRoom, KnownRoomIsReturned) {
this->uart_.room = 7;
this->at_(1000);
EXPECT_EQ(this->uart_.paced_write_room(0), 7u);
this->uart_.room = 0;
EXPECT_EQ(this->uart_.paced_write_room(0), 0u);
}
TEST_F(PacedWriteRoom, UnknownRoomTakesOneLoopInterval) {
this->at_(1000);
// 9600 baud at 10 bits per byte for 16 ms.
EXPECT_EQ(this->uart_.paced_write_room(0), 15u);
}
TEST_F(PacedWriteRoom, EarlyPassTakesTheTimeSinceTheLastWrite) {
this->at_(1005);
EXPECT_EQ(this->uart_.paced_write_room(1000), 4u);
}
TEST_F(PacedWriteRoom, AtLeastOneByte) {
this->at_(1000);
EXPECT_EQ(this->uart_.paced_write_room(1000), 1u);
}
TEST_F(PacedWriteRoom, SpanCapsAtFourSeconds) {
App.set_loop_interval(10000);
this->uart_.set_baud_rate(300);
this->at_(6000);
// 30 bytes/s for 4 s.
EXPECT_EQ(this->uart_.paced_write_room(0), 120u);
}
} // namespace esphome::uart::testing
@@ -0,0 +1,9 @@
valve:
- platform: template
name: Trigger Test Valve
id: trigger_test_valve
optimistic: true
on_open:
- logger.log: Valve fully open
on_closed:
- logger.log: Valve fully closed
@@ -0,0 +1,3 @@
packages:
valve: !include common.yaml
triggers: !include common-triggers.yaml
@@ -0,0 +1,40 @@
esphome:
name: status-momentary-test
host:
api:
actions:
- action: momentary_warning
variables:
length: int
then:
- lambda: "id(probe)->status_momentary_warning(length);"
- action: momentary_error
variables:
length: int
then:
- lambda: "id(probe)->status_momentary_error(length);"
- action: momentary_warning_message
then:
- lambda: 'id(probe)->status_momentary_warning(LOG_STR("probe warning reason"), 300);'
- action: momentary_error_message
then:
- lambda: 'id(probe)->status_momentary_error(LOG_STR("probe error reason"), 300);'
logger:
sensor:
- platform: template
id: probe
update_interval: 24h
lambda: return 1.0;
binary_sensor:
- platform: template
name: Probe Warning
id: probe_warning
lambda: return id(probe)->status_has_warning();
- platform: template
name: Probe Error
id: probe_error
lambda: return id(probe)->status_has_error();
@@ -0,0 +1,87 @@
"""Integration test for status_momentary_warning/error without a name."""
from __future__ import annotations
import asyncio
from aioesphomeapi import BinarySensorState, EntityState
import pytest
from .state_utils import InitialStateHelper
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_status_momentary(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
loop = asyncio.get_running_loop()
warning_logged = loop.create_future()
error_logged = loop.create_future()
def on_log_line(line: str) -> None:
if (
"set Warning flag: probe warning reason" in line
and not warning_logged.done()
):
warning_logged.set_result(True)
if "set Error flag: probe error reason" in line and not error_logged.done():
error_logged.set_result(True)
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
entities, services = await client.list_entities_services()
svc = {s.name: s for s in services}
keys = {e.object_id: e.key for e in entities}
warning_key = keys["probe_warning"]
error_key = keys["probe_error"]
changes: asyncio.Queue[tuple[int, bool, float]] = asyncio.Queue()
def on_state(state: EntityState) -> None:
if isinstance(state, BinarySensorState):
changes.put_nowait((state.key, state.state, loop.time()))
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
await initial_state_helper.wait_for_initial_states()
assert initial_state_helper.initial_states[warning_key].state is False
assert initial_state_helper.initial_states[error_key].state is False
async def next_change(key: int) -> tuple[bool, float]:
while True:
k, value, when = await asyncio.wait_for(changes.get(), timeout=5)
if k == key:
return value, when
# The flag sets at once and clears after the length
for service, key in (
("momentary_warning", warning_key),
("momentary_error", error_key),
):
start = loop.time()
await client.execute_service(svc[service], {"length": 300})
assert (await next_change(key))[0] is True
value, cleared = await next_change(key)
assert value is False
assert cleared - start >= 0.25
# A second call restarts the timeout instead of adding a second one
start = loop.time()
await client.execute_service(svc["momentary_warning"], {"length": 1000})
assert (await next_change(warning_key))[0] is True
await asyncio.sleep(0.6)
await client.execute_service(svc["momentary_warning"], {"length": 1000})
value, cleared = await next_change(warning_key)
assert value is False
assert cleared - start >= 1.4
# A message replaces "unspecified" in the log line
await client.execute_service(svc["momentary_warning_message"], {})
await asyncio.wait_for(warning_logged, timeout=5)
await client.execute_service(svc["momentary_error_message"], {})
await asyncio.wait_for(error_logged, timeout=5)
+11 -2
View File
@@ -242,9 +242,18 @@ def test_get_project_cmakelists_drops_empty_lwip_sources() -> None:
def test_get_project_cmakelists_declares_map_as_link_byproduct() -> None:
"""The link declares the map so size can build in the same ninja run."""
"""The map is a link byproduct only when IDF has not declared it itself."""
content = _render()
assert "BYPRODUCTS ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map" in content
assert (
"get_source_file_property(esphome_map_declared\n"
" ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map GENERATED)"
) in content
assert (
"set(esphome_map_byproducts BYPRODUCTS "
"${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map)"
) in content
assert " ${esphome_map_byproducts}\n WORKING_DIRECTORY" in content
assert "BYPRODUCTS ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map\n" not in content
def test_get_project_cmakelists_uses_supplied_builtin_components() -> None:
+24
View File
@@ -0,0 +1,24 @@
"""Tests for the uart helpers shared by components that own a UART."""
from esphome.components.uart import subtree_references_uart
from esphome.core import ID
def test_finds_a_nested_uart_id() -> None:
config = {"modbus": [{"id": ID("hub"), "uart_id": ID("bus")}]}
assert subtree_references_uart(config, "bus")
def test_ignores_other_uarts_and_other_keys() -> None:
config = {"modbus": [{"uart_id": ID("other")}], "sensor": [{"id": ID("bus")}]}
assert not subtree_references_uart(config, "bus")
def test_finds_another_key() -> None:
config = {"modbus_tcp_uart": [{"tcp_uart_id": ID("bus")}]}
assert subtree_references_uart(config, "bus", "tcp_uart_id")
assert not subtree_references_uart(config, "bus")
def test_handles_scalars() -> None:
assert not subtree_references_uart("bus", "bus")
+77 -6
View File
@@ -943,6 +943,30 @@ async def test_apply_action_call_shape(
assert positions == sorted(positions)
@pytest.mark.asyncio
async def test_apply_target_names_parent(
registries: tuple[Registry, Registry], mock_cg: MockCodegen
) -> None:
"""{parent} in a target is the global-scoped parent, also beside a call object."""
fields = (ApplyCall("set_peer({parent}->id(), {})", (("level", cg.float_),)),)
await _run_apply_action(registries, fields, {"level": 0.5})
assert (
f"::{PARENT_OBJ}->set_peer(::{PARENT_OBJ}->id(), 0.5f);"
in _apply_definition(mock_cg)
)
mock_cg.new_pvariable.reset_mock()
await _run_apply_action(registries, fields, {"level": 0.5}, call="make_call")
assert f"apply_call.set_peer(::{PARENT_OBJ}->id(), 0.5f);" in _apply_definition(
mock_cg
)
mock_cg.new_pvariable.reset_mock()
await _run_apply_condition(registries, "is_peer({parent}->id())", {})
assert (
f"return ::{PARENT_OBJ}->is_peer(::{PARENT_OBJ}->id());"
in _apply_definition(mock_cg)
)
@pytest.mark.asyncio
async def test_apply_field_nested_key_const_fn_and_type_string(
registries: tuple[Registry, Registry], mock_cg: MockCodegen
@@ -979,8 +1003,10 @@ async def test_apply_field_nested_key_const_fn_and_type_string(
def test_apply_registration_checks(registries: tuple[Registry, Registry]) -> None:
with pytest.raises(ValueError, match="2 placeholder"):
ApplyCall("set_range({}, {})", (("low", cg.float_),))
with pytest.raises(ValueError, match="only bare"):
ApplyCall("if ({}) {parent}->reset()", (("reset", cg.bool_),))
with pytest.raises(ValueError, match="only {} and {parent}"):
ApplyCall("if ({}) {other}->reset()", (("reset", cg.bool_),))
assert ApplyCall("if ({}) {parent}->reset()", (("reset", cg.bool_),)).names_parent
assert not ApplyCall("set_flags({{parent}})").names_parent
ApplyCall("set_flags({{{}}})", (("flags", cg.int_),))
with pytest.raises(ValueError, match="each arg is"):
ApplyCall("set_kp({})", (("kp", cg.float_, None, "extra"),))
@@ -1242,12 +1268,57 @@ async def test_trigger_callback_reshapes_and_filters(mock_cg: MockCodegen) -> No
@pytest.mark.asyncio
async def test_trigger_callback_filter_has_no_parent(mock_cg: MockCodegen) -> None:
"""A filter type that names {parent} is rejected up front, a trigger callback has none."""
when = ApplyCall("mode == {}", (("mode", "{parent}::Mode"),))
with pytest.raises(ValueError, match="names {parent}"):
"""A filter that names {parent} is rejected up front when no parent is given."""
for when in (
ApplyCall("mode == {}", (("mode", "{parent}::Mode"),)),
"{parent}->is_failed() == false",
):
with pytest.raises(ValueError, match="names {parent}"):
await build_trigger_callback(
[],
{**TRIGGER_CONF, "mode": 1},
[(cg.int_, "mode")],
forward=[],
when=when,
)
@pytest.mark.asyncio
async def test_trigger_callback_filter_names_parent(mock_cg: MockCodegen) -> None:
"""With the parent given, {parent} in a filter is the global-scoped parent."""
text = _squash(
await build_trigger_callback(
[], {**TRIGGER_CONF, "mode": 1}, [(cg.int_, "mode")], forward=[], when=when
[],
TRIGGER_CONF,
[(cg.int_, "state")],
forward=[],
when="state == 1 && {parent}->is_failed() == false",
parent=MockObj("improv", "->"),
)
)
assert (
"if (!(state == 1 && ::improv->is_failed() == false)) return; "
f"::{NEW_OBJ}->trigger();"
) in text
@pytest.mark.asyncio
async def test_build_callback_automations_when_names_parent(
mock_cg: MockCodegen,
) -> None:
"""A table entry names the parent through {parent}, so it can live at module level."""
entries = (
CallbackAutomation(
"on_open", "add_on_state_callback", when="{parent}->is_fully_open()"
),
)
await build_callback_automations(
MockObj("valve", "->"), {"on_open": [TRIGGER_CONF]}, entries
)
assert _squash(mock_cg.add.call_args.args[0]) == (
"valve->add_on_state_callback([]() -> void { "
f"if (!(::valve->is_fully_open())) return; ::{NEW_OBJ}->trigger(); }})"
)
@pytest.mark.asyncio
+41 -1
View File
@@ -4,7 +4,7 @@ import math
import pytest
from esphome import cpp_generator as cg, cpp_types as ct
from esphome.core import CORE, ID
from esphome.core import CORE, ID, EsphomeError
class TestExpressions:
@@ -829,6 +829,46 @@ class TestSharedProgmemArray:
assert str(a) != str(b)
assert sum("PROGMEM" in str(st) for st in CORE.global_statements) == 2
def test_is_static_pointer_only_for_placement_new(self) -> None:
CORE.config = {}
static = cg.new_Pvariable(ID("static_obj", is_declaration=True, type=ct.uint8))
dynamic = cg.Pvariable(
ID("dynamic_obj", is_declaration=True, type=ct.uint8),
cg.RawExpression("nullptr"),
)
assert cg.is_static_pointer(static)
assert not cg.is_static_pointer(dynamic)
def test_constexpr_and_const_arrays_are_not_shared(self) -> None:
CORE.config = {}
a = cg.shared_progmem_array("table", ct.uint8, [1], constexpr=False)
b = cg.shared_progmem_array("table", ct.uint8, [1])
assert str(a) != str(b)
def test_constexpr_false_rejects_dynamic_pointers(self) -> None:
CORE.config = {}
dynamic = cg.Pvariable(
ID("dynamic_obj", is_declaration=True, type=ct.uint8),
cg.RawExpression("nullptr"),
)
with pytest.raises(EsphomeError, match="dynamic_obj"):
cg.shared_progmem_array(
"table",
ct.uint8.operator("ptr"),
cg.ArrayInitializer(dynamic),
constexpr=False,
)
def test_constexpr_false_emits_a_const_array(self) -> None:
CORE.config = {}
cg.shared_progmem_array("table", ct.uint8, [1], constexpr=False)
(statement,) = (
str(st) for st in CORE.global_statements if "PROGMEM" in str(st)
)
assert statement.startswith(
"ESPHOME_FLASH_CONSTINIT static uint8_t const table[] PROGMEM = {1}"
)
def test_same_contents_different_type_are_separate(self) -> None:
CORE.config = {}
a = cg.shared_progmem_array("table", ct.uint8, [1])