Merge branch 'dev' into component-8byte-optimization

This commit is contained in:
J. Nick Koston
2026-03-29 08:25:57 -10:00
committed by GitHub
337 changed files with 7663 additions and 4481 deletions
+22
View File
@@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
(never change after construction) should be constructor parameters rather than set via setter methods.
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments.
```cpp
// Good - required invariant dependency as constructor parameter
class SourceTextSensor : public text_sensor::TextSensor, public Component {
public:
explicit SourceTextSensor(text::Text *source) : source_(source) {}
protected:
text::Text *source_;
};
```
```cpp
// Bad - required invariant dependency as setter
class SourceTextSensor : public text_sensor::TextSensor, public Component {
public:
void set_source(text::Text *source) { this->source_ = source; }
protected:
text::Text *source_{nullptr};
};
```
* **Component Structure:**
* **Standard Files:**
+1 -1
View File
@@ -1 +1 @@
9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
+1 -1
View File
@@ -154,7 +154,7 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.6
rev: v0.15.8
hooks:
# Run the linter.
- id: ruff
+2
View File
@@ -92,6 +92,7 @@ esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
@@ -459,6 +460,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/spa06_base/* @danielkent-net
esphome/components/spa06_i2c/* @danielkent-net
esphome/components/spa06_spi/* @danielkent-net
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
+10 -1
View File
@@ -1046,7 +1046,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
):
from esphome.components.api.client import run_logs
return run_logs(config, network_devices)
return run_logs(
config,
network_devices,
subscribe_states=not getattr(args, "no_states", False),
)
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
from esphome import mqtt
@@ -1664,6 +1668,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_logs.add_argument(
"--no-states",
action="store_true",
help="Do not show entity state changes in log output.",
)
parser_discover = subparsers.add_parser(
"discover",
+49 -2
View File
@@ -137,6 +137,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation")
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
@@ -413,13 +416,16 @@ async def if_action_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
has_else = CONF_ELSE in config
# Prepend HasElse bool to template arguments: IfAction<HasElse, Ts...>
if_template_arg = cg.TemplateArguments(has_else, *template_arg)
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
var = cg.new_Pvariable(action_id, if_template_arg, condition)
if CONF_THEN in config:
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
if CONF_ELSE in config:
if has_else:
actions = await build_action_list(config[CONF_ELSE], template_arg, args)
cg.add(var.add_else(actions))
return var
@@ -658,3 +664,44 @@ async def build_automation(
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
return obj
async def build_callback_automation(
parent: MockObj,
callback_method: str,
args: TemplateArgsType,
config: ConfigType,
forwarder: MockObj | MockObjClass | None = None,
) -> None:
"""Build an Automation and register it as a callback on the parent.
Eliminates the need for a Trigger wrapper object by registering the
automation's trigger() directly as a callback on the parent component.
Uses template forwarder structs so the compiler deduplicates the operator()
body across all call sites with the same signature. The forwarder must be
pointer-sized (single Automation* field) to fit inline in Callback::ctx_
and avoid heap allocation.
:param parent: The component object (e.g., button, sensor).
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
:param args: Automation template args as list of (type, name) tuples.
:param config: The automation config dict.
:param forwarder: Optional forwarder type to use instead of the default
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
a single Automation pointer (e.g., TriggerOnTrueForwarder).
"""
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
# Use template forwarder structs for deduplication. The compiler generates
# one operator() per forwarder type; different automation pointers are just
# data in the struct.
if forwarder is None:
forwarder = TriggerForwarder.template(templ)
# RawExpression for aggregate init — both forwarder and obj are codegen
# MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
+10 -9
View File
@@ -53,6 +53,13 @@ def get_project_cmakelists() -> str:
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")]
extra_compile_options = "\n".join(
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
for compile_def in compile_defs
)
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
@@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{extra_compile_options}
project({CORE.name})
"""
@@ -70,10 +80,6 @@ def get_component_cmakelists(minimal: bool = False) -> str:
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
@@ -104,11 +110,6 @@ idf_component_register(
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile definitions
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
{compile_defs_str}
)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
@@ -10,7 +10,6 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -34,39 +33,9 @@ CONF_ON_READY = "on_ready"
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
StateTrigger = alarm_control_panel_ns.class_(
"StateTrigger", automation.Trigger.template()
)
TriggeredTrigger = alarm_control_panel_ns.class_(
"TriggeredTrigger", automation.Trigger.template()
)
ClearedTrigger = alarm_control_panel_ns.class_(
"ClearedTrigger", automation.Trigger.template()
)
ArmingTrigger = alarm_control_panel_ns.class_(
"ArmingTrigger", automation.Trigger.template()
)
PendingTrigger = alarm_control_panel_ns.class_(
"PendingTrigger", automation.Trigger.template()
)
ArmedHomeTrigger = alarm_control_panel_ns.class_(
"ArmedHomeTrigger", automation.Trigger.template()
)
ArmedNightTrigger = alarm_control_panel_ns.class_(
"ArmedNightTrigger", automation.Trigger.template()
)
ArmedAwayTrigger = alarm_control_panel_ns.class_(
"ArmedAwayTrigger", automation.Trigger.template()
)
DisarmedTrigger = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template()
)
ChimeTrigger = alarm_control_panel_ns.class_(
"ChimeTrigger", automation.Trigger.template()
)
ReadyTrigger = alarm_control_panel_ns.class_(
"ReadyTrigger", automation.Trigger.template()
)
StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder")
StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder")
AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
@@ -89,61 +58,17 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTAlarmControlPanelComponent
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
}
),
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
}
),
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
}
),
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
}
),
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
}
),
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
}
),
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
}
),
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
}
),
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
}
),
cv.Optional(CONF_ON_READY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMING): automation.validate_automation({}),
cv.Optional(CONF_ON_PENDING): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation({}),
cv.Optional(CONF_ON_DISARMED): automation.validate_automation({}),
cv.Optional(CONF_ON_CLEARED): automation.validate_automation({}),
cv.Optional(CONF_ON_CHIME): automation.validate_automation({}),
cv.Optional(CONF_ON_READY): automation.validate_automation({}),
}
)
)
@@ -189,38 +114,39 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_TRIGGERED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PENDING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_HOME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_NIGHT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_AWAY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISARMED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder
)
_STATE_ENTER_MAP = {
CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED,
CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING,
CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING,
CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME,
CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT,
CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY,
CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED,
}
for conf_key, state_enum in _STATE_ENTER_MAP.items():
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=StateEnterForwarder.template(state_enum),
)
for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_cleared_callback", [], conf
)
for conf in config.get(CONF_ON_CHIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_chime_callback", [], conf
)
for conf in config.get(CONF_ON_READY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_ready_callback", [], conf
)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
@@ -31,12 +31,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call();
// Single state callback - listeners receive the new state as an argument
this->state_callback_.call(state);
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
@@ -145,8 +145,8 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state
LazyCallbackManager<void()> state_callback_{};
// state callback - passes the new state to listeners
LazyCallbackManager<void(AlarmControlPanelState)> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
LazyCallbackManager<void()> cleared_callback_{};
// chime callback
@@ -5,60 +5,27 @@
namespace esphome::alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() { this->trigger(); });
/// Callback forwarder that triggers an Automation<> on any state change.
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
struct StateAnyForwarder {
Automation<> *automation;
void operator()(AlarmControlPanelState /*state*/) const { this->automation->trigger(); }
};
/// Callback forwarder that triggers an Automation<> only when the alarm enters a specific state.
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
template<AlarmControlPanelState State> struct StateEnterForwarder {
Automation<> *automation;
void operator()(AlarmControlPanelState state) const {
if (state == State)
this->automation->trigger();
}
};
/// Template trigger that fires when entering a specific state
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public:
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() {
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
// Type aliases for state-specific triggers
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); });
}
};
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
}
};
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
}
};
static_assert(sizeof(StateAnyForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<StateAnyForwarder>);
static_assert(sizeof(StateEnterForwarder<ACP_STATE_TRIGGERED>) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<StateEnterForwarder<ACP_STATE_TRIGGERED>>);
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public:
+1
View File
@@ -2512,6 +2512,7 @@ message ListEntitiesInfraredResponse {
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified)
}
// Command to transmit infrared/RF data using raw timings
@@ -1549,6 +1549,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
auto *infrared = static_cast<infrared::Infrared *>(entity);
ListEntitiesInfraredResponse msg;
msg.capabilities = infrared->get_capability_flags();
msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz();
return fill_and_encode_entity_info(infrared, msg, conn, remaining_size);
}
#endif
+9 -3
View File
@@ -2249,10 +2249,14 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
return true;
}
void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_uint64(1, this->address, true);
buffer.encode_sint32(2, this->rssi, true);
buffer.write_raw_byte(8);
buffer.encode_varint_raw_64(this->address);
buffer.write_raw_byte(16);
buffer.encode_varint_raw(encode_zigzag32(this->rssi));
buffer.encode_uint32(3, this->address_type);
buffer.encode_bytes(4, this->data, this->data_len, true);
buffer.write_raw_byte(34);
buffer.encode_varint_raw(this->data_len);
buffer.encode_raw(this->data, this->data_len);
}
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
uint32_t size = 0;
@@ -3653,6 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_uint32(8, this->capabilities);
buffer.encode_uint32(9, this->receiver_frequency);
}
uint32_t ListEntitiesInfraredResponse::calculate_size() const {
uint32_t size = 0;
@@ -3668,6 +3673,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const {
size += ProtoSize::calc_uint32(1, this->device_id);
#endif
size += ProtoSize::calc_uint32(1, this->capabilities);
size += ProtoSize::calc_uint32(1, this->receiver_frequency);
return size;
}
#endif
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -9,8 +9,8 @@ namespace esphome::api {
static const char *const TAG = "api.service";
#ifdef HAS_PROTO_MESSAGE_DUMP
void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) {
ESP_LOGVV(TAG, "send_message %s: %s", name, dump);
void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) {
ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);
}
void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) {
DumpBuffer dump_buf;
+1 -1
View File
@@ -12,7 +12,7 @@ class APIServerConnectionBase {
public:
#ifdef HAS_PROTO_MESSAGE_DUMP
protected:
void log_send_message_(const char *name, const char *dump);
void log_send_message_(const LogString *name, const char *dump);
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
void log_receive_message_(const LogString *name);
+21 -11
View File
@@ -46,10 +46,8 @@ void APIServer::setup() {
#ifndef USE_API_NOISE_PSK_FROM_YAML
// Only load saved PSK if not set from YAML
SavedNoisePsk noise_pref_saved{};
if (this->noise_pref_.load(&noise_pref_saved)) {
if (this->load_and_apply_noise_psk_()) {
ESP_LOGD(TAG, "Loaded saved Noise PSK");
this->set_noise_psk(noise_pref_saved.psk);
}
#endif
#endif
@@ -110,7 +108,7 @@ void APIServer::setup() {
this->last_connected_ = App.get_loop_component_start_time();
// Set warning status if reboot timeout is enabled
if (this->reboot_timeout_ != 0) {
this->status_set_warning();
this->status_set_warning(LOG_STR("waiting for client connection"));
}
}
@@ -189,7 +187,7 @@ void APIServer::remove_client_(size_t client_index) {
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -514,7 +512,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
#ifdef USE_API_NOISE
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
const LogString *fail_log_msg, bool make_active) {
if (!this->noise_pref_.save(&new_psk)) {
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
return false;
@@ -526,9 +524,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
}
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
if (make_active) {
this->set_timeout(100, [this, active_psk]() {
this->set_timeout(100, [this]() {
// Re-read the PSK from preferences rather than capturing the 32-byte array
// in the lambda (which would exceed std::function SBO and heap-allocate).
if (!this->load_and_apply_noise_psk_()) {
ESP_LOGW(TAG, "Failed to load saved PSK for activation");
return;
}
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(active_psk);
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req);
@@ -538,6 +541,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
return true;
}
bool APIServer::load_and_apply_noise_psk_() {
SavedNoisePsk saved{};
if (!this->noise_pref_.load(&saved))
return false;
this->set_noise_psk(saved.psk);
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called
@@ -552,7 +563,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
}
SavedNoisePsk new_saved_psk{psk};
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
make_active);
#endif
}
@@ -564,8 +575,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
return false;
#else
SavedNoisePsk empty_psk{};
psk_t empty{};
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
make_active);
#endif
}
+3 -1
View File
@@ -239,7 +239,9 @@ class APIServer final : public Component,
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);
bool make_active);
// Load saved PSK from preferences and apply it. Returns true on success.
bool load_and_apply_noise_psk_();
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
+14 -4
View File
@@ -32,7 +32,11 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
async def async_run_logs(
config: dict[str, Any],
addresses: list[str],
subscribe_states: bool = True,
) -> None:
"""Run the logs command in the event loop."""
conf = config["api"]
name = config["esphome"]["name"]
@@ -89,14 +93,20 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
config, raw_line, backtrace_state=backtrace_state
)
stop = await async_run(cli, on_log, name=name)
stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states)
try:
await asyncio.Event().wait()
finally:
await stop()
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
def run_logs(
config: dict[str, Any],
addresses: list[str],
subscribe_states: bool = True,
) -> None:
"""Run the logs command."""
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, addresses))
asyncio.run(
async_run_logs(config, addresses, subscribe_states=subscribe_states)
)
+37 -16
View File
@@ -5,6 +5,7 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <cassert>
@@ -152,8 +153,7 @@ class ProtoVarInt {
#endif
};
// Forward declarations for decode_to_message and related encoding helpers
class ProtoDecodableMessage;
// Forward declarations for encoding helpers
class ProtoMessage;
class ProtoSize;
@@ -166,16 +166,9 @@ class ProtoLengthDelimited {
const uint8_t *data() const { return this->value_; }
size_t size() const { return this->length_; }
/**
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
*
* This method allows decoding without templates, enabling use in contexts
* where the message type is not known at compile time. The ProtoDecodableMessage's
* decode() method will be called with the raw data and length.
*
* @param msg The ProtoDecodableMessage instance to decode into
*/
void decode_to_message(ProtoDecodableMessage &msg) const;
/// Decode the length-delimited data into a message instance.
/// Template preserves concrete type so decode() resolves statically.
template<typename T> void decode_to_message(T &msg) const;
protected:
const uint8_t *const value_;
@@ -236,6 +229,17 @@ class ProtoWriteBuffer {
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
/// Write a single precomputed tag byte. Tag must be < 128.
inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(1);
*this->pos_++ = b;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(len);
std::memcpy(this->pos_, data, len);
this->pos_ += len;
}
/// Write a precomputed tag byte + 32-bit value in one operation.
/// Tag must be a single-byte varint (< 128). No zero check.
inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE {
@@ -408,6 +412,23 @@ class DumpBuffer {
return *this;
}
/// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms)
DumpBuffer &append_p(const char *str) {
if (str) {
#ifdef USE_ESP8266
append_p_esp8266(str);
#else
append_impl_(str, strlen(str));
#endif
}
return *this;
}
#ifdef USE_ESP8266
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
void append_p_esp8266(const char *str);
#endif
const char *c_str() const { return buf_; }
size_t size() const { return pos_; }
@@ -453,7 +474,7 @@ class ProtoMessage {
uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP
virtual const char *dump_to(DumpBuffer &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
virtual const LogString *message_name() const { return LOG_STR("unknown"); }
#endif
#ifndef USE_HOST
@@ -468,7 +489,7 @@ class ProtoMessage {
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
virtual void decode(const uint8_t *buffer, size_t length);
void decode(const uint8_t *buffer, size_t length);
/**
* Count occurrences of a repeated field in a protobuf buffer.
@@ -704,8 +725,8 @@ template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(u
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const {
// Template decode_to_message - preserves concrete type so decode() resolves statically
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
msg.decode(this->value_, this->length_);
}
@@ -2,11 +2,9 @@ import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ANGLE,
CONF_GAIN,
CONF_ID,
CONF_MAGNITUDE,
CONF_POSITION,
CONF_STATUS,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_MAGNET,
@@ -21,7 +19,6 @@ DEPENDENCIES = ["as5600"]
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
@@ -89,18 +86,6 @@ async def to_code(config):
if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
if angle_config := config.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(var.set_angle_sensor(sens))
if raw_angle_config := config.get(CONF_RAW_ANGLE):
sens = await sensor.new_sensor(raw_angle_config)
cg.add(var.set_raw_angle_sensor(sens))
if position_config := config.get(CONF_POSITION):
sens = await sensor.new_sensor(position_config)
cg.add(var.set_position_sensor(sens))
if raw_position_config := config.get(CONF_RAW_POSITION):
sens = await sensor.new_sensor(raw_position_config)
cg.add(var.set_raw_position_sensor(sens))
@@ -25,27 +25,10 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
void AS5600Sensor::dump_config() {
LOG_SENSOR("", "AS5600 Sensor", this);
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
if (this->angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_);
}
if (this->raw_angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_);
}
if (this->position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Position Sensor", this->position_sensor_);
}
if (this->raw_position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
}
if (this->gain_sensor_ != nullptr) {
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
}
if (this->magnitude_sensor_ != nullptr) {
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
}
if (this->status_sensor_ != nullptr) {
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
}
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
LOG_UPDATE_INTERVAL(this);
}
@@ -15,9 +15,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
void update() override;
void dump_config() override;
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
this->raw_position_sensor_ = raw_position_sensor;
}
@@ -28,9 +25,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
protected:
sensor::Sensor *angle_sensor_{nullptr};
sensor::Sensor *raw_angle_sensor_{nullptr};
sensor::Sensor *position_sensor_{nullptr};
sensor::Sensor *raw_position_sensor_{nullptr};
sensor::Sensor *gain_sensor_{nullptr};
sensor::Sensor *magnitude_sensor_{nullptr};
+2 -2
View File
@@ -550,8 +550,8 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
}
float ATM90E32Component::get_phase_angle_(uint8_t phase) {
uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
return (val > 180) ? (float) (val - 360.0f) : (float) val;
float val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0f;
return (val > 180.0f) ? val - 360.0f : val;
}
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
-1
View File
@@ -134,7 +134,6 @@ class ATM90E32Component : public PollingComponent,
void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; }
#endif
uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
int32_t last_periodic_millis = millis();
protected:
#ifdef USE_NUMBER
+2 -2
View File
@@ -204,7 +204,7 @@ async def to_code(config):
add_idf_component(
name="esphome/esp-audio-libs",
ref="2.0.3",
ref="2.0.4",
)
data = _get_data()
@@ -214,4 +214,4 @@ async def to_code(config):
cg.add_define("USE_AUDIO_MP3_SUPPORT")
if data.opus_support:
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
add_idf_component(name="esphome/micro-opus", ref="0.3.5")
add_idf_component(name="esphome/micro-opus", ref="0.3.6")
+18 -43
View File
@@ -120,10 +120,6 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_(
BinarySensorPtr = BinarySensor.operator("ptr")
# Triggers
PressTrigger = binary_sensor_ns.class_("PressTrigger", automation.Trigger.template())
ReleaseTrigger = binary_sensor_ns.class_(
"ReleaseTrigger", automation.Trigger.template()
)
ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template())
DoubleClickTrigger = binary_sensor_ns.class_(
"DoubleClickTrigger", automation.Trigger.template()
@@ -132,13 +128,6 @@ MultiClickTrigger = binary_sensor_ns.class_(
"MultiClickTrigger", automation.Trigger.template(), cg.Component
)
MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
StateTrigger = binary_sensor_ns.class_(
"StateTrigger", automation.Trigger.template(bool)
)
StateChangeTrigger = binary_sensor_ns.class_(
"StateChangeTrigger",
automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)),
)
BinarySensorPublishAction = binary_sensor_ns.class_(
"BinarySensorPublishAction", automation.Action
@@ -458,16 +447,8 @@ _BINARY_SENSOR_SCHEMA = (
): cv.boolean,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger),
}
),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger),
}
),
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
cv.Optional(CONF_ON_CLICK): cv.All(
automation.validate_automation(
{
@@ -509,16 +490,8 @@ _BINARY_SENSOR_SCHEMA = (
): cv.positive_time_period_milliseconds,
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation({}),
}
)
)
@@ -556,13 +529,14 @@ def binary_sensor_schema(
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_binary_sensor_automations(var, config):
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_RELEASE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
for conf in config.get(CONF_ON_CLICK, []):
trigger = cg.new_Pvariable(
@@ -593,13 +567,14 @@ async def _build_binary_sensor_automations(var, config):
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
await automation.build_callback_automation(
var,
"add_full_state_callback",
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
@@ -45,7 +45,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
ESP_LOGV(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
}
return false;
+1 -1
View File
@@ -124,7 +124,7 @@ def set_reference_values(config):
config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF)
config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF)
config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF)
config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF)
config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF)
else:
vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF)
r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1)
-1
View File
@@ -56,7 +56,6 @@ void BM8563::read_time() {
ESPTime rtc_time;
this->get_time_(rtc_time);
this->get_date_(rtc_time);
rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid
ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
@@ -276,8 +276,8 @@ void BME68xBSEC2Component::run_() {
}
if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) {
uint32_t meas_dur = 0;
meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
bme68x_get_conf(&bme68x_conf, &this->bme68x_);
uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
this->trigger_time_ns_ = curr_time_ns;
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
@@ -469,14 +469,18 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
}
bool BMP581Component::reset_() {
// - activates interface (only relevant for SPI mode)
// - writes reset command to the command register
// - waits for sensor to complete reset
// - activates interface (only relevant for SPI mode)
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
// activates communication interface (SPI only)
this->activate_interface();
// writes reset command to BMP's command register
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
ESP_LOGE(TAG, "Failed to write reset command");
return false;
}
@@ -484,6 +488,9 @@ bool BMP581Component::reset_() {
// - round up to 3 ms
delay(3);
// reactivates communication interface after reset (SPI only)
this->activate_interface();
// read interrupt status register
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
@@ -491,7 +498,7 @@ bool BMP581Component::reset_() {
return false;
}
// Power-On-Reboot bit is asserted if sensor successfully reset
// power-On-Reboot bit is asserted if sensor successfully reset
return this->int_status_.bit.por;
}
@@ -87,6 +87,9 @@ class BMP581Component : public PollingComponent {
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
// Interface activation function. Only used for SPI interface; no-op for I2C.
virtual void activate_interface() {}
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
@@ -0,0 +1,73 @@
#include <cstdint>
#include <cstddef>
#include "bmp581_spi.h"
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome::bmp581_spi {
static const char *const TAG = "bmp581_spi";
// OR (|) register with BMP_SPI_READ for read
inline constexpr uint8_t BMP_SPI_READ = 0x80;
// AND (&) register with BMP_SPI_WRITE for write
inline constexpr uint8_t BMP_SPI_WRITE = 0x7F;
void BMP581SPIComponent::dump_config() {
BMP581Component::dump_config();
LOG_SPI_DEVICE(this);
}
void BMP581SPIComponent::setup() {
this->spi_setup();
BMP581Component::setup();
}
void BMP581SPIComponent::activate_interface() {
// - forces the device into SPI mode using a dummy read
uint8_t dummy_read = 0;
this->bmp_read_byte(bmp581_base::BMP581_CHIP_ID, &dummy_read);
}
// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used
// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read).
// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
// The expressions BMP_SPI_READ (| with register) and BMP_SPI_WRITE (& with register)
// are defined for readability.
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp581-ds004.pdf
bool BMP581SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) {
this->enable();
this->transfer_byte(a_register | BMP_SPI_READ);
*data = this->transfer_byte(0);
this->disable();
return true;
}
bool BMP581SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) {
this->enable();
this->transfer_byte(a_register & BMP_SPI_WRITE);
this->transfer_byte(data);
this->disable();
return true;
}
bool BMP581SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
this->enable();
this->transfer_byte(a_register | BMP_SPI_READ);
this->read_array(data, len);
this->disable();
return true;
}
bool BMP581SPIComponent::bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) {
this->enable();
this->transfer_byte(a_register & BMP_SPI_WRITE);
this->write_array(data, len);
this->disable();
return true;
}
} // namespace esphome::bmp581_spi
@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome::bmp581_spi {
// BMP581 is technically compatible with SPI Mode0 and Mode3. Default to Mode3.
class BMP581SPIComponent : public esphome::bmp581_base::BMP581Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
public:
void setup() override;
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override;
bool bmp_write_byte(uint8_t a_register, uint8_t data) override;
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
void dump_config() override;
protected:
void activate_interface() override;
};
} // namespace esphome::bmp581_spi
+48
View File
@@ -0,0 +1,48 @@
import logging
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.spi import CONF_SPI_MODE
import esphome.config_validation as cv
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
AUTO_LOAD = ["bmp581_base"]
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
DEPENDENCIES = ["spi"]
_LOGGER = logging.getLogger(__name__)
VALID_SPI_MODES = {
0: "MODE0",
"0": "MODE0",
"MODE0": "MODE0",
3: "MODE3",
"3": "MODE3",
"MODE3": "MODE3",
}
bmp581_ns = cg.esphome_ns.namespace("bmp581_spi")
BMP581SPIComponent = bmp581_ns.class_(
"BMP581SPIComponent", cg.PollingComponent, spi.SPIDevice
)
def check_spi_mode(config):
spi_mode = config.get(CONF_SPI_MODE)
if spi_mode not in VALID_SPI_MODES:
raise cv.Invalid("BMP581 only supports SPI mode 3")
return config
CONFIG_SCHEMA = cv.All(
CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend(
{cv.GenerateID(): cv.declare_id(BMP581SPIComponent)}
),
check_spi_mode,
)
async def to_code(config):
var = await to_code_base(config)
await spi.register_spi_device(var, config)
+4 -12
View File
@@ -10,7 +10,6 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_PRESS,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_IDENTIFY,
@@ -41,10 +40,6 @@ ButtonPtr = Button.operator("ptr")
PressAction = button_ns.class_("PressAction", automation.Action)
ButtonPressTrigger = button_ns.class_(
"ButtonPressTrigger", automation.Trigger.template()
)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
@@ -55,11 +50,7 @@ _BUTTON_SCHEMA = (
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger),
}
),
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
}
)
)
@@ -91,8 +82,9 @@ def button_schema(
@setup_entity("button")
async def setup_button_core_(var, config):
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_press_callback", [], conf
)
setup_device_class(config)
+26 -27
View File
@@ -46,45 +46,45 @@ constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = {
void ClimateCall::perform() {
this->parent_->control_callback_.call(*this);
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
this->validate_();
if (this->mode_.has_value()) {
const LogString *mode_s = climate_mode_to_string(*this->mode_);
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
}
if (this->custom_fan_mode_ != nullptr) {
this->fan_mode_.reset();
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_);
ESP_LOGV(TAG, " Custom Fan: %s", this->custom_fan_mode_);
}
if (this->fan_mode_.has_value()) {
this->custom_fan_mode_ = nullptr;
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
ESP_LOGV(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
}
if (this->custom_preset_ != nullptr) {
this->preset_.reset();
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_);
}
if (this->preset_.has_value()) {
this->custom_preset_ = nullptr;
const LogString *preset_s = climate_preset_to_string(*this->preset_);
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
}
if (this->swing_mode_.has_value()) {
const LogString *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_);
ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s));
ESP_LOGV(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s));
}
if (this->target_temperature_.has_value()) {
ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_);
ESP_LOGV(TAG, " Target Temperature: %.2f", *this->target_temperature_);
}
if (this->target_temperature_low_.has_value()) {
ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_);
ESP_LOGV(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_);
}
if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
ESP_LOGV(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
}
if (this->target_humidity_.has_value()) {
ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_);
ESP_LOGV(TAG, " Target Humidity: %.0f", *this->target_humidity_);
}
this->parent_->control(*this);
}
@@ -367,7 +367,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
return recovered;
}
void Climate::save_state_() {
void Climate::save_state_(const ClimateTraits &traits) {
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY)
#pragma GCC diagnostic ignored "-Wclass-memaccess"
@@ -382,7 +382,6 @@ void Climate::save_state_() {
#endif
state.mode = this->mode;
auto traits = this->get_traits();
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
state.target_temperature_low = this->target_temperature_low;
@@ -435,43 +434,43 @@ void Climate::save_state_() {
}
void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGV(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
ESP_LOGV(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
}
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
ESP_LOGV(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
}
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_);
ESP_LOGV(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_);
}
if (traits.get_supports_presets() && this->preset.has_value()) {
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
}
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_);
}
if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
ESP_LOGV(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature);
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
this->target_temperature_high);
} else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
ESP_LOGV(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
ESP_LOGV(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
// Send state to frontend
@@ -480,7 +479,7 @@ void Climate::publish_state() {
ControllerRegistry::notify_climate_update(this);
#endif
// Save state
this->save_state_();
this->save_state_(traits);
}
ClimateTraits Climate::get_traits() {
+2 -1
View File
@@ -335,7 +335,8 @@ class Climate : public EntityBase {
/** Internal method to save the state of the climate device to recover memory. This is automatically
* called from publish_state()
*/
void save_state_();
void save_state_(const ClimateTraits &traits);
void save_state_() { this->save_state_(this->traits()); }
void dump_traits_(const char *tag);
+2
View File
@@ -13,10 +13,12 @@ CONF_DATA_BITS = "data_bits"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_LIBRETINY = "libretiny"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_PARITY = "parity"
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_STOP_BITS = "stop_bits"
+1 -1
View File
@@ -7,7 +7,7 @@ namespace copy {
static const char *const TAG = "copy.lock";
void CopyLock::setup() {
source_->add_on_state_callback([this]() { this->publish_state(source_->state); });
source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); });
traits.set_assumed_state(source_->traits.get_assumed_state());
traits.set_requires_code(source_->traits.get_requires_code());
+13 -13
View File
@@ -68,24 +68,24 @@ CoverCall &CoverCall::set_tilt(float tilt) {
return *this;
}
void CoverCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
auto traits = this->parent_->get_traits();
this->validate_();
if (this->stop_) {
ESP_LOGD(TAG, " Command: STOP");
ESP_LOGV(TAG, " Command: STOP");
}
if (this->position_.has_value()) {
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f);
ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f);
} else {
ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_)));
ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_)));
}
}
if (this->tilt_.has_value()) {
ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f);
ESP_LOGV(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f);
}
if (this->toggle_.has_value()) {
ESP_LOGD(TAG, " Command: TOGGLE");
ESP_LOGV(TAG, " Command: TOGGLE");
}
this->parent_->control(*this);
}
@@ -143,23 +143,23 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGV(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);
ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f);
} else {
if (this->position == COVER_OPEN) {
ESP_LOGD(TAG, " State: OPEN");
ESP_LOGV(TAG, " State: OPEN");
} else if (this->position == COVER_CLOSED) {
ESP_LOGD(TAG, " State: CLOSED");
ESP_LOGV(TAG, " State: CLOSED");
} else {
ESP_LOGD(TAG, " State: UNKNOWN");
ESP_LOGV(TAG, " State: UNKNOWN");
}
}
if (traits.get_supports_tilt()) {
ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
}
ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
this->state_callback_.call();
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)
+5 -5
View File
@@ -30,7 +30,7 @@ void DateEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
ESP_LOGV(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);
@@ -83,16 +83,16 @@ void DateCall::validate_() {
void DateCall::perform() {
this->validate_();
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (this->year_.has_value()) {
ESP_LOGD(TAG, " Year: %d", *this->year_);
ESP_LOGV(TAG, " Year: %d", *this->year_);
}
if (this->month_.has_value()) {
ESP_LOGD(TAG, " Month: %d", *this->month_);
ESP_LOGV(TAG, " Month: %d", *this->month_);
}
if (this->day_.has_value()) {
ESP_LOGD(TAG, " Day: %d", *this->day_);
ESP_LOGV(TAG, " Day: %d", *this->day_);
}
this->parent_->control(*this);
}
@@ -45,7 +45,7 @@ void DateTimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_,
ESP_LOGV(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_,
this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
@@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const {
obj.year = this->year_;
obj.month = this->month_;
obj.day_of_month = this->day_;
obj.day_of_week = 0;
obj.day_of_year = 0;
obj.is_dst = false;
obj.hour = this->hour_;
obj.minute = this->minute_;
obj.second = this->second_;
@@ -124,25 +127,25 @@ void DateTimeCall::validate_() {
void DateTimeCall::perform() {
this->validate_();
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (this->year_.has_value()) {
ESP_LOGD(TAG, " Year: %d", *this->year_);
ESP_LOGV(TAG, " Year: %d", *this->year_);
}
if (this->month_.has_value()) {
ESP_LOGD(TAG, " Month: %d", *this->month_);
ESP_LOGV(TAG, " Month: %d", *this->month_);
}
if (this->day_.has_value()) {
ESP_LOGD(TAG, " Day: %d", *this->day_);
ESP_LOGV(TAG, " Day: %d", *this->day_);
}
if (this->hour_.has_value()) {
ESP_LOGD(TAG, " Hour: %d", *this->hour_);
ESP_LOGV(TAG, " Hour: %d", *this->hour_);
}
if (this->minute_.has_value()) {
ESP_LOGD(TAG, " Minute: %d", *this->minute_);
ESP_LOGV(TAG, " Minute: %d", *this->minute_);
}
if (this->second_.has_value()) {
ESP_LOGD(TAG, " Second: %d", *this->second_);
ESP_LOGV(TAG, " Second: %d", *this->second_);
}
this->parent_->control(*this);
}
+5 -5
View File
@@ -26,7 +26,7 @@ void TimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_);
ESP_LOGV(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);
@@ -52,15 +52,15 @@ void TimeCall::validate_() {
void TimeCall::perform() {
this->validate_();
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (this->hour_.has_value()) {
ESP_LOGD(TAG, " Hour: %d", *this->hour_);
ESP_LOGV(TAG, " Hour: %d", *this->hour_);
}
if (this->minute_.has_value()) {
ESP_LOGD(TAG, " Minute: %d", *this->minute_);
ESP_LOGV(TAG, " Minute: %d", *this->minute_);
}
if (this->second_.has_value()) {
ESP_LOGD(TAG, " Second: %d", *this->second_);
ESP_LOGV(TAG, " Second: %d", *this->second_);
}
this->parent_->control(*this);
}
+5 -13
View File
@@ -2,16 +2,13 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_TRIGGER_ID, CONF_VOLUME
from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@glmnet"]
dfplayer_ns = cg.esphome_ns.namespace("dfplayer")
DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component)
DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_(
"DFPlayerFinishedPlaybackTrigger", automation.Trigger.template()
)
DFPlayerIsPlayingCondition = dfplayer_ns.class_(
"DFPlayerIsPlayingCondition", automation.Condition
)
@@ -58,13 +55,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DFPlayer),
cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DFPlayerFinishedPlaybackTrigger
),
}
),
cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}),
}
).extend(uart.UART_DEVICE_SCHEMA)
)
@@ -79,8 +70,9 @@ async def to_code(config):
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_finished_playback_callback", [], conf
)
@automation.register_action(
-7
View File
@@ -171,12 +171,5 @@ template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts.
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
};
class DFPlayerFinishedPlaybackTrigger : public Trigger<> {
public:
explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) {
parent->add_on_finished_playback_callback([this]() { this->trigger(); });
}
};
} // namespace dfplayer
} // namespace esphome
-3
View File
@@ -40,11 +40,8 @@ void DS1307Component::read_time() {
.hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10),
.day_of_week = uint8_t(ds1307_.reg.weekday),
.day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10),
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
.month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10),
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
.is_dst = false, // not used
.timestamp = 0 // overwritten by recalc_timestamp_utc(false)
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid()) {
+30 -6
View File
@@ -97,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_RELEASE = "release"
CONF_SRAM1_AS_IRAM = "sram1_as_iram"
CONF_SUBTYPE = "subtype"
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
@@ -378,12 +379,11 @@ FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values(
def set_core_data(config):
cpu_frequency = config.get(CONF_CPU_FREQUENCY, None)
variant = config[CONF_VARIANT]
# if not specified in config, set to 160MHz if supported, the fastest otherwise
# if not specified in config, default to the maximum supported frequency
# (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz)
if cpu_frequency is None:
choices = CPU_FREQUENCIES[variant]
if "160MHZ" in choices:
cpu_frequency = "160MHZ"
elif "360MHZ" in choices:
if variant == VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE):
cpu_frequency = "360MHZ"
else:
cpu_frequency = choices[-1]
@@ -690,9 +690,15 @@ ARDUINO_IDF_VERSION_LOOKUP = {
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 3, "1"),
"latest": cv.Version(5, 5, 3, "1"),
"dev": cv.Version(5, 5, 3, "1"),
"dev": cv.Version(5, 5, 4),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(
5, 5, 4
): "https://github.com/pioarduino/platform-espressif32.git#develop",
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
@@ -884,6 +890,13 @@ def final_validate(config):
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
)
)
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
errs.append(
cv.Invalid(
f"'{CONF_SRAM1_AS_IRAM}' is only supported on {VARIANT_ESP32}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
)
)
if (
config[CONF_VARIANT] != VARIANT_ESP32P4
and config.get(CONF_ENGINEERING_SAMPLE) is not None
@@ -1131,6 +1144,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
*ESP32_CHIP_REVISIONS
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
@@ -1579,7 +1593,7 @@ async def to_code(config):
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]:
cg.add_define("USE_FULL_PRINTF")
else:
for symbol in ("vprintf", "printf", "fprintf"):
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
else:
cg.add_build_flag("-DUSE_ARDUINO")
@@ -1655,6 +1669,16 @@ async def to_code(config):
for rev, flag in ESP32_CHIP_REVISIONS.items():
add_idf_sdkconfig_option(flag, rev == min_rev)
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
# Use SRAM1 region as IRAM on ESP32 (original) variant
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
# WARNING: If the device has an old bootloader (pre-v5.1), the app will fail to boot.
# A USB flash will update the bootloader automatically. OTA updates do not.
# See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/performance/ram-usage.html
if variant == VARIANT_ESP32 and conf[CONF_ADVANCED][CONF_SRAM1_AS_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM", True)
cg.add_define("USE_ESP32_SRAM1_AS_IRAM")
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv")
@@ -14,6 +14,8 @@ _ESP32C3_SPI_PSRAM_PINS = {
17: "SPIQ",
}
_ESP32C3_USB_JTAG_PINS = {18, 19}
_ESP32C3_STRAPPING_PINS = {2, 8, 9}
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +28,12 @@ def esp32_c3_validate_gpio_pin(value: int) -> int:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})"
)
if value in _ESP32C3_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
return value
+9 -1
View File
@@ -18,7 +18,9 @@ _ESP32C6_SPI_PSRAM_PINS = {
30: "SPID",
}
_ESP32C6_STRAPPING_PINS = {8, 9, 15}
_ESP32C6_USB_JTAG_PINS = {12, 13}
_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15}
_LOGGER = logging.getLogger(__name__)
@@ -30,6 +32,12 @@ def esp32_c6_validate_gpio_pin(value: int) -> int:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})"
)
if value in _ESP32C6_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
return value
+3 -3
View File
@@ -9,7 +9,7 @@ _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
_ESP32H2_USB_JTAG_PINS = {26, 27}
_ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25}
_ESP32H2_STRAPPING_PINS = {8, 9, 25}
_LOGGER = logging.getLogger(__name__)
@@ -26,8 +26,8 @@ def esp32_h2_validate_gpio_pin(value: int) -> int:
)
if value in _ESP32H2_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
"To use this pin as GPIO, USB-Serial-JTAG will be disabled.",
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
+2 -2
View File
@@ -20,8 +20,8 @@ def esp32_p4_validate_gpio_pin(value: int) -> int:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if value in _ESP32P4_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
"To use this pin as GPIO, USB-Serial-JTAG will be disabled.",
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
+15 -7
View File
@@ -5,7 +5,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP_32S3_SPI_PSRAM_PINS = {
_ESP32S3_SPI_PSRAM_PINS = {
26: "SPICS1",
27: "SPIHD",
28: "SPIWP",
@@ -15,7 +15,7 @@ _ESP_32S3_SPI_PSRAM_PINS = {
32: "SPID",
}
_ESP_32_ESP32_S3R8_PSRAM_PINS = {
_ESP32S3R8_PSRAM_PINS = {
33: "SPIIO4",
34: "SPIIO5",
35: "SPIIO6",
@@ -23,7 +23,9 @@ _ESP_32_ESP32_S3R8_PSRAM_PINS = {
37: "SPIDQS",
}
_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46}
_ESP32S3_USB_JTAG_PINS = {19, 20}
_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46}
_LOGGER = logging.getLogger(__name__)
@@ -32,11 +34,11 @@ def esp32_s3_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 48:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)")
if value in _ESP_32S3_SPI_PSRAM_PINS:
if value in _ESP32S3_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})"
f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})"
)
if value in _ESP_32_ESP32_S3R8_PSRAM_PINS:
if value in _ESP32S3R8_PSRAM_PINS:
_LOGGER.warning(
"GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models",
value,
@@ -46,6 +48,12 @@ def esp32_s3_validate_gpio_pin(value: int) -> int:
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.")
if value in _ESP32S3_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
return value
@@ -61,5 +69,5 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP_32S3_STRAPPING_PINS, _LOGGER)
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
return value
+11 -6
View File
@@ -2,10 +2,11 @@
* Linker wrap stubs for FILE*-based printf functions.
*
* ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference
* fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r
* (~11 KB). This is a separate implementation from _svfprintf_r (used by
* snprintf/vsnprintf) that handles FILE* stream I/O with buffering and
* locking.
* fprintf(), printf(), vprintf(), and vfprintf() which pull in the full
* printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on
* picolibc's vfprintf). This is a separate implementation from the one
* used by snprintf/vsnprintf that handles FILE* stream I/O with buffering
* and locking.
*
* ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(),
* so the SDK's vprintf() path is dead code at runtime. The fprintf()
@@ -70,11 +71,15 @@ int __wrap_printf(const char *fmt, ...) {
return len;
}
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
char buf[PRINTF_BUFFER_SIZE];
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
}
int __wrap_fprintf(FILE *stream, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
char buf[PRINTF_BUFFER_SIZE];
int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
int len = __wrap_vfprintf(stream, fmt, ap);
va_end(ap);
return len;
}
@@ -350,7 +350,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// For V3_WITHOUT_CACHE, we already set fast params before connecting
// No need to update them again here
this->log_event_("Searching for services");
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
esp_ble_gattc_search_service(esp_gattc_if, param->open.conn_id, nullptr);
break;
}
case ESP_GATTC_CONNECT_EVT: {
@@ -70,6 +70,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
public:
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override {
// If the listener is already set, do nothing
@@ -115,6 +116,7 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...
public:
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); }
@@ -159,7 +159,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
break;
}
case ESP_GATTS_STOP_EVT: {
if (param->start.service_handle == this->handle_) {
if (param->stop.service_handle == this->handle_) {
this->state_ = STOPPED;
}
break;
+40 -12
View File
@@ -115,10 +115,12 @@ ETHERNET_TYPES = {
"JL1101": EthernetType.ETHERNET_TYPE_JL1101,
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
"W5100": EthernetType.ETHERNET_TYPE_W5100,
"W5500": EthernetType.ETHERNET_TYPE_W5500,
"OPENETH": EthernetType.ETHERNET_TYPE_OPENETH,
"DM9051": EthernetType.ETHERNET_TYPE_DM9051,
"LAN8670": EthernetType.ETHERNET_TYPE_LAN8670,
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
}
# PHY types that need compile-time defines for conditional compilation
@@ -131,9 +133,11 @@ _PHY_TYPE_TO_DEFINE = {
"JL1101": "USE_ETHERNET_JL1101",
"KSZ8081": "USE_ETHERNET_KSZ8081",
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
"W5100": "USE_ETHERNET_W5100",
"W5500": "USE_ETHERNET_W5500",
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
"ENC28J60": "USE_ETHERNET_ENC28J60",
}
@@ -155,11 +159,22 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
}
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
# These types are always external IDF components (never built-in to ESP-IDF)
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"}
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = ["W5500"]
RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"}
_RP2040_SPI_LIBRARIES = {
"W5100": "lwIP_w5100",
"W5500": "lwIP_w5500",
"ENC28J60": "lwIP_enc28j60",
}
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
@@ -220,7 +235,18 @@ def _validate(config):
if CORE.is_esp32:
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
if _is_framework_spi_polling_mode_supported():
# ENC28J60 driver does not support polling mode - interrupt is required
if config[CONF_TYPE] == "ENC28J60":
if CONF_POLLING_INTERVAL in config:
raise cv.Invalid(
f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. "
f"'{CONF_INTERRUPT_PIN}' is required."
)
if CONF_INTERRUPT_PIN not in config:
raise cv.Invalid(
f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60."
)
elif _is_framework_spi_polling_mode_supported():
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
raise cv.Invalid(
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
@@ -277,7 +303,7 @@ def _validate(config):
)
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, "
f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -364,9 +390,11 @@ CONFIG_SCHEMA = cv.All(
"JL1101": RMII_SCHEMA,
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W5500": SPI_SCHEMA,
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"ENC28J60": SPI_SCHEMA,
"LAN8670": RMII_SCHEMA,
},
upper=True,
@@ -502,7 +530,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
cg.add_define("USE_ETHERNET_SPI")
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
if idf_version() < cv.Version(6, 0, 0):
# ENC28J60 was never built-in to IDF, so it has no Kconfig option
if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60":
add_idf_sdkconfig_option(
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
)
@@ -533,12 +562,11 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
include_builtin_idf_component("esp_eth")
if config[CONF_TYPE] == "LAN8670":
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
if idf_version() >= cv.Version(6, 0, 0) and (
if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS:
component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]]
add_idf_component(name=component.name, ref=component.version)
elif idf_version() >= cv.Version(6, 0, 0) and (
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE])
):
add_idf_component(name=component.name, ref=component.version)
@@ -555,7 +583,7 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library("lwIP_w5500", None)
cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None)
def _final_validate_rmii_pins(config: ConfigType) -> None:
@@ -23,7 +23,15 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif // USE_ESP32
#ifdef USE_RP2040
#if defined(USE_ETHERNET_W5500)
#include <W5500lwIP.h>
#elif defined(USE_ETHERNET_W5100)
#include <W5100lwIP.h>
#elif defined(USE_ETHERNET_ENC28J60)
#include <ENC28J60lwIP.h>
#else
#error "Unsupported RP2040 SPI Ethernet type"
#endif
#endif
namespace esphome::ethernet {
@@ -53,10 +61,12 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_JL1101,
ETHERNET_TYPE_KSZ8081,
ETHERNET_TYPE_KSZ8081RNA,
ETHERNET_TYPE_W5100,
ETHERNET_TYPE_W5500,
ETHERNET_TYPE_OPENETH,
ETHERNET_TYPE_DM9051,
ETHERNET_TYPE_LAN8670,
ETHERNET_TYPE_ENC28J60,
};
struct ManualIP {
@@ -215,7 +225,20 @@ class EthernetComponent final : public Component {
#ifdef USE_RP2040
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
#if defined(USE_ETHERNET_W5100)
static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time
#else
static constexpr uint32_t RESET_DELAY_MS = 10;
#endif
#if defined(USE_ETHERNET_W5500)
Wiznet5500lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W5100)
Wiznet5100lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_ENC28J60)
ENC28J60lwIP *eth_{nullptr};
#else
#error "Unsupported RP2040 SPI Ethernet type"
#endif
uint32_t last_link_check_{0};
uint8_t clk_pin_;
uint8_t miso_pin_;
@@ -44,6 +44,11 @@
#include "esp_eth_phy_lan867x.h"
#endif
// ENC28J60 header exists on all IDF versions (always an external component)
#ifdef USE_ETHERNET_ENC28J60
#include "esp_eth_enc28j60.h"
#endif
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
@@ -194,25 +199,27 @@ void EthernetComponent::setup() {
.post_cb = nullptr,
};
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
#elif defined(USE_ETHERNET_ENC28J60)
eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
w5500_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
dm9051_config.poll_period_ms = this->polling_interval_;
#endif
#elif defined(USE_ETHERNET_ENC28J60)
enc28j60_config.int_gpio_num = this->interrupt_pin_;
// ENC28J60 does not support poll_period_ms
#endif
phy_config.phy_addr = this->phy_addr_spi_;
@@ -300,19 +307,24 @@ void EthernetComponent::setup() {
#endif
#endif
#ifdef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
case ETHERNET_TYPE_W5500: {
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
case ETHERNET_TYPE_DM9051: {
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
break;
}
#elif defined(USE_ETHERNET_ENC28J60)
case ETHERNET_TYPE_ENC28J60: {
mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config);
this->phy_ = esp_eth_phy_new_enc28j60(&phy_config);
break;
}
#endif
#endif
default: {
@@ -405,15 +417,18 @@ void EthernetComponent::dump_config() {
eth_type = "KSZ8081RNA";
break;
#endif
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
case ETHERNET_TYPE_DM9051:
eth_type = "DM9051";
break;
#elif defined(USE_ETHERNET_ENC28J60)
case ETHERNET_TYPE_ENC28J60:
eth_type = "ENC28J60";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
@@ -31,11 +31,18 @@ void EthernetComponent::setup() {
reset_pin.digital_write(false);
delay(1); // NOLINT
reset_pin.digital_write(true);
delay(10); // NOLINT - wait for W5500 to initialize after reset
// W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms
delay(RESET_DELAY_MS); // NOLINT
}
// Create the W5500 device instance
// Create the SPI Ethernet device instance
#if defined(USE_ETHERNET_W5500)
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W5100)
this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_ENC28J60)
this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#endif
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
@@ -61,7 +68,7 @@ void EthernetComponent::setup() {
}
if (!success) {
ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet");
ESP_LOGE(TAG, "Failed to initialize Ethernet");
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
this->eth_ = nullptr;
this->mark_failed();
@@ -76,8 +83,8 @@ void EthernetComponent::setup() {
// or via GPIO interrupt when one is provided.
// Don't set started_ here — let the link polling in loop() set it
// when the W5500 link is actually up. Setting it prematurely causes
// a "Starting → Stopped → Starting" log sequence because the W5500
// when the link is actually up. Setting it prematurely causes
// a "Starting → Stopped → Starting" log sequence because the chip
// needs time after begin() before the PHY link is ready.
}
@@ -85,14 +92,21 @@ void EthernetComponent::loop() {
// On RP2040, we need to poll connection state since there are no events.
const uint32_t now = App.get_loop_component_start_time();
// Throttle link/IP polling to avoid excessive SPI transactions from linkStatus()
// which reads the W5500 PHY register via SPI on every call.
// Throttle link/IP polling to avoid excessive SPI transactions.
// W5500/ENC28J60 read PHY register via SPI on every linkStatus() call.
// W5100 can't detect link state, so we skip the SPI read and assume link-up.
// connected() reads netif->ip_addr without LwIPLock, but this is a single
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
// value, which is benign for polling.
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
this->last_link_check_ = now;
#if defined(USE_ETHERNET_W5100)
// W5100 can't detect link (isLinkDetectable() returns false), so linkStatus()
// returns Unknown — assume link is up after successful begin()
bool link_up = true;
#else
bool link_up = this->eth_->linkStatus() == LinkON;
#endif
bool has_ip = this->eth_->connected();
if (!link_up) {
@@ -164,9 +178,17 @@ void EthernetComponent::loop() {
}
void EthernetComponent::dump_config() {
const char *type_str = "Unknown";
#if defined(USE_ETHERNET_W5500)
type_str = "W5500";
#elif defined(USE_ETHERNET_W5100)
type_str = "W5100";
#elif defined(USE_ETHERNET_ENC28J60)
type_str = "ENC28J60";
#endif
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: W5500\n"
" Type: %s\n"
" Connected: %s\n"
" CLK Pin: %u\n"
" MISO Pin: %u\n"
@@ -174,7 +196,7 @@ void EthernetComponent::dump_config() {
" CS Pin: %u\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
this->dump_connect_params_();
}
@@ -216,13 +238,18 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
// W5500 is always full duplex
// W5100, W5500, and ENC28J60 are full-duplex on RP2040
return ETH_DUPLEX_FULL;
}
eth_speed_t EthernetComponent::get_link_speed() {
// W5500 is always 100Mbps
#ifdef USE_ETHERNET_ENC28J60
// ENC28J60 is 10Mbps only
return ETH_SPEED_10M;
#else
// W5100 and W5500 are 100Mbps
return ETH_SPEED_100M;
#endif
}
bool EthernetComponent::powerdown() {
+4 -10
View File
@@ -10,7 +10,6 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_EVENT,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_BUTTON,
DEVICE_CLASS_DOORBELL,
@@ -41,8 +40,6 @@ EventPtr = Event.operator("ptr")
TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action)
EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template())
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_EVENT_SCHEMA = (
@@ -53,11 +50,7 @@ _EVENT_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
cv.GenerateID(): cv.declare_id(Event),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_ON_EVENT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(EventTrigger),
}
),
cv.Optional(CONF_ON_EVENT): automation.validate_automation({}),
}
)
)
@@ -92,8 +85,9 @@ def event_schema(
@setup_entity("event")
async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
await automation.build_callback_automation(
var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf
)
cg.add(var.set_event_types(event_types))
+1 -1
View File
@@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) {
return;
}
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(StringRef(found));
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include <utility>
#include "esphome/core/automation.h"
#include "ezo.h"
namespace esphome {
namespace ezo {
class LedTrigger : public Trigger<bool> {
public:
explicit LedTrigger(EZOSensor *ezo) {
ezo->add_led_state_callback([this](bool value) { this->trigger(value); });
}
};
class CustomTrigger : public Trigger<std::string> {
public:
explicit CustomTrigger(EZOSensor *ezo) {
ezo->add_custom_callback([this](const std::string &value) { this->trigger(value); });
}
};
class TTrigger : public Trigger<std::string> {
public:
explicit TTrigger(EZOSensor *ezo) {
ezo->add_t_callback([this](const std::string &value) { this->trigger(value); });
}
};
class CalibrationTrigger : public Trigger<std::string> {
public:
explicit CalibrationTrigger(EZOSensor *ezo) {
ezo->add_calibration_callback([this](const std::string &value) { this->trigger(value); });
}
};
class SlopeTrigger : public Trigger<std::string> {
public:
explicit SlopeTrigger(EZOSensor *ezo) {
ezo->add_slope_callback([this](const std::string &value) { this->trigger(value); });
}
};
class DeviceInformationTrigger : public Trigger<std::string> {
public:
explicit DeviceInformationTrigger(EZOSensor *ezo) {
ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); });
}
};
} // namespace ezo
} // namespace esphome
+25 -69
View File
@@ -2,7 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID
from esphome.const import CONF_ID
CODEOWNERS = ["@ssieb"]
@@ -21,61 +21,16 @@ EZOSensor = ezo_ns.class_(
"EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
CustomTrigger = ezo_ns.class_(
"CustomTrigger", automation.Trigger.template(cg.std_string)
)
TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string))
SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string))
CalibrationTrigger = ezo_ns.class_(
"CalibrationTrigger", automation.Trigger.template(cg.std_string)
)
DeviceInformationTrigger = ezo_ns.class_(
"DeviceInformationTrigger", automation.Trigger.template(cg.std_string)
)
LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_))
CONFIG_SCHEMA = (
sensor.sensor_schema(EZOSensor)
.extend(
{
cv.Optional(CONF_ON_CUSTOM): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger),
}
),
cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger),
}
),
cv.Optional(CONF_ON_SLOPE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger),
}
),
cv.Optional(CONF_ON_T): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger),
}
),
cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DeviceInformationTrigger
),
}
),
cv.Optional(CONF_ON_LED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger),
}
),
cv.Optional(CONF_ON_CUSTOM): automation.validate_automation({}),
cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation({}),
cv.Optional(CONF_ON_SLOPE): automation.validate_automation({}),
cv.Optional(CONF_ON_T): automation.validate_automation({}),
cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation({}),
cv.Optional(CONF_ON_LED): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("60s"))
@@ -90,25 +45,26 @@ async def to_code(config):
await i2c.register_i2c_device(var, config)
for conf in config.get(CONF_ON_CUSTOM, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_custom_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_LED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
await automation.build_callback_automation(
var, "add_led_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_DEVICE_INFORMATION, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_device_infomation_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_SLOPE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_slope_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_CALIBRATION, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_calibration_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_T, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_t_callback", [(cg.std_string, "x")], conf
)
+6 -15
View File
@@ -1,10 +1,9 @@
from esphome.automation import Trigger, build_automation, validate_automation
from esphome import automation
import esphome.codegen as cg
from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_TRIGGER_ID,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
@@ -18,7 +17,6 @@ CODEOWNERS = ["@anatoly-savchenkov"]
factory_reset_ns = cg.esphome_ns.namespace("factory_reset")
FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component)
FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component)
CONF_MAX_DELAY = "max_delay"
CONF_RESETS_REQUIRED = "resets_required"
@@ -55,11 +53,7 @@ CONFIG_SCHEMA = cv.All(
),
),
cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int,
cv.Optional(CONF_ON_INCREMENT): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger),
}
),
cv.Optional(CONF_ON_INCREMENT): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA),
_validate,
@@ -88,12 +82,9 @@ async def to_code(config):
)
await cg.register_component(var, config)
for conf in config.get(CONF_ON_INCREMENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await build_automation(
trigger,
[
(cg.uint8, "x"),
(cg.uint8, "target"),
],
await automation.build_callback_automation(
var,
"add_increment_callback",
[(cg.uint8, "x"), (cg.uint8, "target")],
conf,
)
@@ -30,12 +30,6 @@ class FactoryResetComponent : public Component {
uint8_t required_count_; // The number of boot attempts before fast boot is enabled
};
class FastBootTrigger : public Trigger<uint8_t, uint8_t> {
public:
explicit FastBootTrigger(FactoryResetComponent *parent) {
parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); });
}
};
} // namespace esphome::factory_reset
#endif // !defined(USE_RP2040) && !defined(USE_HOST)
+11 -11
View File
@@ -44,22 +44,22 @@ FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) {
}
void FanCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
ESP_LOGV(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
this->validate_();
if (this->binary_state_.has_value()) {
ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_));
ESP_LOGV(TAG, " State: %s", ONOFF(*this->binary_state_));
}
if (this->oscillating_.has_value()) {
ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_));
ESP_LOGV(TAG, " Oscillating: %s", YESNO(*this->oscillating_));
}
if (this->speed_.has_value()) {
ESP_LOGD(TAG, " Speed: %d", *this->speed_);
ESP_LOGV(TAG, " Speed: %d", *this->speed_);
}
if (this->direction_.has_value()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
}
if (this->preset_mode_ != nullptr) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_);
}
this->parent_.control(*this);
}
@@ -196,21 +196,21 @@ void Fan::apply_preset_mode_(const FanCall &call) {
void Fan::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
ESP_LOGV(TAG,
"'%s' >>\n"
" State: %s",
this->name_.c_str(), ONOFF(this->state));
if (traits.supports_speed()) {
ESP_LOGD(TAG, " Speed: %d", this->speed);
ESP_LOGV(TAG, " Speed: %d", this->speed);
}
if (traits.supports_oscillation()) {
ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating));
ESP_LOGV(TAG, " Oscillating: %s", YESNO(this->oscillating));
}
if (traits.supports_direction()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
}
if (this->preset_mode_ != nullptr) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_);
}
this->state_callback_.call();
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
+36 -106
View File
@@ -21,7 +21,6 @@ from esphome.const import (
CONF_SENSING_PIN,
CONF_SPEED,
CONF_STATE,
CONF_TRIGGER_ID,
)
CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"]
@@ -38,38 +37,6 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_(
"FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice
)
FingerScanStartTrigger = fingerprint_grow_ns.class_(
"FingerScanStartTrigger", automation.Trigger.template()
)
FingerScanMatchedTrigger = fingerprint_grow_ns.class_(
"FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16)
)
FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
"FingerScanUnmatchedTrigger", automation.Trigger.template()
)
FingerScanMisplacedTrigger = fingerprint_grow_ns.class_(
"FingerScanMisplacedTrigger", automation.Trigger.template()
)
FingerScanInvalidTrigger = fingerprint_grow_ns.class_(
"FingerScanInvalidTrigger", automation.Trigger.template()
)
EnrollmentScanTrigger = fingerprint_grow_ns.class_(
"EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16)
)
EnrollmentDoneTrigger = fingerprint_grow_ns.class_(
"EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16)
)
EnrollmentFailedTrigger = fingerprint_grow_ns.class_(
"EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16)
)
EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action)
CancelEnrollmentAction = fingerprint_grow_ns.class_(
"CancelEnrollmentAction", automation.Action
@@ -125,62 +92,22 @@ CONFIG_SCHEMA = cv.All(
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_PASSWORD): cv.uint32_t,
cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t,
cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanStartTrigger
),
}
),
cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation({}),
cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanMatchedTrigger
),
}
{}
),
cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanUnmatchedTrigger
),
}
{}
),
cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanMisplacedTrigger
),
}
{}
),
cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanInvalidTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentScanTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentDoneTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentFailedTrigger
),
}
{}
),
cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation({}),
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}),
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("500ms"))
@@ -214,40 +141,43 @@ async def to_code(config):
cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms))
for conf in config.get(CONF_ON_FINGER_SCAN_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_finger_scan_start_callback", [], conf
)
for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf
await automation.build_callback_automation(
var,
"add_on_finger_scan_matched_callback",
[(cg.uint16, "finger_id"), (cg.uint16, "confidence")],
conf,
)
for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_finger_scan_unmatched_callback", [], conf
)
for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_finger_scan_misplaced_callback", [], conf
)
for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_finger_scan_invalid_callback", [], conf
)
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf
await automation.build_callback_automation(
var,
"add_on_enrollment_scan_callback",
[(cg.uint8, "scan_num"), (cg.uint16, "finger_id")],
conf,
)
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
await automation.build_callback_automation(
var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf
)
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
await automation.build_callback_automation(
var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf
)
@automation.register_action(
@@ -210,64 +210,6 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
CallbackManager<void(uint16_t)> enrollment_failed_callback_;
};
class FingerScanStartTrigger : public Trigger<> {
public:
explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_start_callback([this]() { this->trigger(); });
}
};
class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> {
public:
explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_matched_callback(
[this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); });
}
};
class FingerScanUnmatchedTrigger : public Trigger<> {
public:
explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); });
}
};
class FingerScanMisplacedTrigger : public Trigger<> {
public:
explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); });
}
};
class FingerScanInvalidTrigger : public Trigger<> {
public:
explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); });
}
};
class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
public:
explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {
parent->add_on_enrollment_scan_callback(
[this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); });
}
};
class EnrollmentDoneTrigger : public Trigger<uint16_t> {
public:
explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) {
parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); });
}
};
class EnrollmentFailedTrigger : public Trigger<uint16_t> {
public:
explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) {
parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); });
}
};
template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
public:
TEMPLATABLE_VALUE(uint16_t, finger_id)
@@ -23,9 +23,8 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) {
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
bool new_state = arg->isr_pin_.digital_read();
if (new_state != arg->last_state_) {
if (new_state != arg->state_) {
arg->state_ = new_state;
arg->last_state_ = new_state;
arg->changed_ = true;
// Wake up the component from its disabled loop state
if (arg->component_ != nullptr) {
@@ -34,28 +33,27 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
}
}
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) {
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
pin->setup();
this->isr_pin_ = pin->to_isr();
this->component_ = component;
// Read initial state
this->last_state_ = pin->digital_read();
this->state_ = this->last_state_;
this->state_ = pin->digital_read();
// Attach interrupt - from this point on, any changes will be caught by the interrupt
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type);
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_);
}
void GPIOBinarySensor::setup() {
if (this->use_interrupt_ && !this->pin_->is_internal()) {
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
this->use_interrupt_ = false;
this->store_.use_interrupt_ = false;
}
if (this->use_interrupt_) {
if (this->store_.use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this->interrupt_type_, this);
this->store_.setup(internal_pin, this);
this->publish_initial_state(this->store_.get_state());
} else {
this->pin_->setup();
@@ -66,14 +64,14 @@ void GPIOBinarySensor::setup() {
void GPIOBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_)));
if (this->use_interrupt_) {
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_)));
ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_)));
if (this->store_.use_interrupt_) {
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_)));
}
}
void GPIOBinarySensor::loop() {
if (this->use_interrupt_) {
if (this->store_.use_interrupt_) {
if (this->store_.is_changed()) {
// Clear the flag immediately to minimize the window where we might miss changes
this->store_.clear_changed();
@@ -8,10 +8,10 @@
namespace esphome {
namespace gpio {
// Store class for ISR data (no vtables, ISR-safe)
// Store class for ISR data and configuration (no vtables, ISR-safe)
class GPIOBinarySensorStore {
public:
void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component);
void setup(InternalGPIOPin *pin, Component *component);
static void gpio_intr(GPIOBinarySensorStore *arg);
@@ -32,11 +32,13 @@ class GPIOBinarySensorStore {
}
protected:
friend class GPIOBinarySensor;
ISRInternalGPIOPin isr_pin_;
volatile bool state_{false};
volatile bool last_state_{false};
volatile bool changed_{false};
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
volatile bool state_{false};
volatile bool changed_{false};
bool use_interrupt_{true};
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
};
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
@@ -44,9 +46,9 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
// No destructor needed: ESPHome components are created at boot and live forever.
// Interrupts are only detached on reboot when memory is cleared anyway.
void set_pin(GPIOPin *pin) { pin_ = pin; }
void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; }
void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; }
void set_pin(GPIOPin *pin) { this->pin_ = pin; }
void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; }
void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Setup pin
@@ -59,8 +61,6 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
protected:
GPIOPin *pin_;
bool use_interrupt_{true};
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
GPIOBinarySensorStore store_;
};
@@ -32,6 +32,7 @@ async def to_code(config):
cg.add(var.set_pin(pin))
if CONF_INTERLOCK in config:
cg.add_define("USE_GPIO_SWITCH_INTERLOCK")
interlock = []
for it in config[CONF_INTERLOCK]:
lock = await cg.get_variable(it)
@@ -5,7 +5,9 @@ namespace esphome {
namespace gpio {
static const char *const TAG = "switch.gpio";
#ifdef USE_GPIO_SWITCH_INTERLOCK
static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0;
#endif
float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; }
void GPIOSwitch::setup() {
@@ -28,6 +30,7 @@ void GPIOSwitch::setup() {
void GPIOSwitch::dump_config() {
LOG_SWITCH("", "GPIO Switch", this);
LOG_PIN(" Pin: ", this->pin_);
#ifdef USE_GPIO_SWITCH_INTERLOCK
if (!this->interlock_.empty()) {
ESP_LOGCONFIG(TAG, " Interlocks:");
for (auto *lock : this->interlock_) {
@@ -36,8 +39,10 @@ void GPIOSwitch::dump_config() {
ESP_LOGCONFIG(TAG, " %s", lock->get_name().c_str());
}
}
#endif
}
void GPIOSwitch::write_state(bool state) {
#ifdef USE_GPIO_SWITCH_INTERLOCK
if (state != this->inverted_) {
// Turning ON, check interlocking
@@ -64,11 +69,15 @@ void GPIOSwitch::write_state(bool state) {
// re-activations
this->cancel_timeout(INTERLOCK_TIMEOUT_ID);
}
#endif
this->pin_->digital_write(state);
this->publish_state(state);
}
#ifdef USE_GPIO_SWITCH_INTERLOCK
void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; }
#endif
} // namespace gpio
} // namespace esphome
@@ -18,15 +18,19 @@ class GPIOSwitch final : public switch_::Switch, public Component {
void setup() override;
void dump_config() override;
#ifdef USE_GPIO_SWITCH_INTERLOCK
void set_interlock(const std::initializer_list<Switch *> &interlock);
void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
#endif
protected:
void write_state(bool state) override;
GPIOPin *pin_;
#ifdef USE_GPIO_SWITCH_INTERLOCK
FixedVector<Switch *> interlock_;
uint32_t interlock_wait_time_{0};
#endif
};
} // namespace gpio
-4
View File
@@ -16,10 +16,6 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
val.year = tiny_gps.date.year();
val.month = tiny_gps.date.month();
val.day_of_month = tiny_gps.date.day();
// Set these to valid value for recalc_timestamp_utc - it's not used for calculation
val.day_of_week = 1;
val.day_of_year = 1;
val.hour = tiny_gps.time.hour();
val.minute = tiny_gps.time.minute();
val.second = tiny_gps.time.second();
+18 -44
View File
@@ -22,7 +22,6 @@ from esphome.const import (
CONF_SUPPORTED_SWING_MODES,
CONF_TARGET_TEMPERATURE,
CONF_TEMPERATURE_STEP,
CONF_TRIGGER_ID,
CONF_VISUAL,
CONF_WIFI,
)
@@ -122,21 +121,6 @@ SUPPORTED_HON_CONTROL_METHODS = {
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
}
HaierAlarmStartTrigger = haier_ns.class_(
"HaierAlarmStartTrigger",
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
HaierAlarmEndTrigger = haier_ns.class_(
"HaierAlarmEndTrigger",
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
StatusMessageTrigger = haier_ns.class_(
"StatusMessageTrigger",
automation.Trigger.template(cg.const_char_ptr, cg.size_t),
)
def validate_visual(config):
if CONF_VISUAL in config:
@@ -203,13 +187,7 @@ def _base_config_schema(class_: MockObjClass) -> cv.Schema:
cv.Optional(
CONF_ANSWER_TIMEOUT,
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
StatusMessageTrigger
),
}
),
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation({}),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
@@ -264,19 +242,9 @@ CONFIG_SCHEMA = cv.All(
f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead"
),
cv.Optional(CONF_ON_ALARM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HaierAlarmStartTrigger
),
}
),
cv.Optional(CONF_ON_ALARM_END): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HaierAlarmEndTrigger
),
}
{}
),
cv.Optional(CONF_ON_ALARM_END): automation.validate_automation({}),
}
),
},
@@ -530,19 +498,25 @@ async def to_code(config):
var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE])
)
for conf in config.get(CONF_ON_ALARM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
await automation.build_callback_automation(
var,
"add_alarm_start_callback",
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
conf,
)
for conf in config.get(CONF_ON_ALARM_END, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
await automation.build_callback_automation(
var,
"add_alarm_end_callback",
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
conf,
)
for conf in config.get(CONF_ON_STATUS_MESSAGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], conf
await automation.build_callback_automation(
var,
"add_status_message_callback",
[(cg.const_char_ptr, "data"), (cg.size_t, "data_size")],
conf,
)
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.31")
-7
View File
@@ -177,12 +177,5 @@ class HaierClimateBase : public esphome::Component,
ESPPreferenceObject base_rtc_;
};
class StatusMessageTrigger : public Trigger<const char *, size_t> {
public:
explicit StatusMessageTrigger(HaierClimateBase *parent) {
parent->add_status_message_callback([this](const char *data, size_t data_size) { this->trigger(data, data_size); });
}
};
} // namespace haier
} // namespace esphome
+1 -1
View File
@@ -748,7 +748,7 @@ void HonClimate::update_sub_sensor_(SubSensorType type, float value) {
if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) {
size_t index = (size_t) type;
if ((this->sub_sensors_[index] != nullptr) &&
((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value)))
((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->get_raw_state() != value)))
this->sub_sensors_[index]->publish_state(value);
}
}
-16
View File
@@ -200,21 +200,5 @@ class HonClimate : public HaierClimateBase {
SwitchState quiet_mode_state_{SwitchState::OFF};
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {
public:
explicit HaierAlarmStartTrigger(HonClimate *parent) {
parent->add_alarm_start_callback(
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
}
};
class HaierAlarmEndTrigger : public Trigger<uint8_t, const char *> {
public:
explicit HaierAlarmEndTrigger(HonClimate *parent) {
parent->add_alarm_end_callback(
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
}
};
} // namespace haier
} // namespace esphome
+28 -81
View File
@@ -8,7 +8,6 @@ from esphome.const import (
CONF_NAME,
CONF_ON_ENROLLMENT_DONE,
CONF_ON_ENROLLMENT_FAILED,
CONF_TRIGGER_ID,
)
CODEOWNERS = ["@OnFreund"]
@@ -28,33 +27,6 @@ HlkFm22xComponent = hlk_fm22x_ns.class_(
"HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice
)
FaceScanMatchedTrigger = hlk_fm22x_ns.class_(
"FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string)
)
FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_(
"FaceScanUnmatchedTrigger", automation.Trigger.template()
)
FaceScanInvalidTrigger = hlk_fm22x_ns.class_(
"FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8)
)
FaceInfoTrigger = hlk_fm22x_ns.class_(
"FaceInfoTrigger",
automation.Trigger.template(
cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16
),
)
EnrollmentDoneTrigger = hlk_fm22x_ns.class_(
"EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8)
)
EnrollmentFailedTrigger = hlk_fm22x_ns.class_(
"EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8)
)
EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action)
DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action)
DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action)
@@ -65,46 +37,14 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HlkFm22xComponent),
cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FaceScanMatchedTrigger
),
}
),
cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation({}),
cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FaceScanUnmatchedTrigger
),
}
),
cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FaceScanInvalidTrigger
),
}
),
cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger),
}
),
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentDoneTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentFailedTrigger
),
}
{}
),
cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation({}),
cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation({}),
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}),
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("50ms"))
@@ -118,23 +58,27 @@ async def to_code(config):
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf
await automation.build_callback_automation(
var,
"add_on_face_scan_matched_callback",
[(cg.int16, "face_id"), (cg.std_string, "name")],
conf,
)
for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_face_scan_unmatched_callback", [], conf
)
for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
await automation.build_callback_automation(
var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf
)
for conf in config.get(CONF_ON_FACE_INFO, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
await automation.build_callback_automation(
var,
"add_on_face_info_callback",
[
(cg.int16, "status"),
(cg.int16, "left"),
@@ -149,14 +93,17 @@ async def to_code(config):
)
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf
await automation.build_callback_automation(
var,
"add_on_enrollment_done_callback",
[(cg.int16, "face_id"), (cg.uint8, "direction")],
conf,
)
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
await automation.build_callback_automation(
var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf
)
@automation.register_action(
-46
View File
@@ -141,52 +141,6 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
CallbackManager<void(uint8_t)> enrollment_failed_callback_;
};
class FaceScanMatchedTrigger : public Trigger<int16_t, std::string> {
public:
explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_scan_matched_callback(
[this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); });
}
};
class FaceScanUnmatchedTrigger : public Trigger<> {
public:
explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); });
}
};
class FaceScanInvalidTrigger : public Trigger<uint8_t> {
public:
explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); });
}
};
class FaceInfoTrigger : public Trigger<int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t> {
public:
explicit FaceInfoTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_info_callback(
[this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch,
int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); });
}
};
class EnrollmentDoneTrigger : public Trigger<int16_t, uint8_t> {
public:
explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) {
parent->add_on_enrollment_done_callback(
[this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); });
}
};
class EnrollmentFailedTrigger : public Trigger<uint8_t> {
public:
explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) {
parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); });
}
};
template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
TEMPLATABLE_VALUE(std::string, name)
+1
View File
@@ -128,6 +128,7 @@ SCAN_WIRINGS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
"SCAN_1_8_32PX_FULL": Hub75ScanWiring.SCAN_1_8_32PX_FULL,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
}
+4
View File
@@ -101,9 +101,13 @@ class InfraredTraits {
bool get_supports_receiver() const { return this->supports_receiver_; }
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
uint32_t get_receiver_frequency_hz() const { return this->receiver_frequency_hz_; }
void set_receiver_frequency_hz(uint32_t freq) { this->receiver_frequency_hz_ = freq; }
protected:
bool supports_transmitter_{false};
bool supports_receiver_{false};
uint32_t receiver_frequency_hz_{0}; // Demodulation frequency of the IR receiver in Hz (0 = unspecified)
};
/// Infrared - Base class for infrared remote control implementations
+1 -1
View File
@@ -229,7 +229,7 @@ void Inkplate::eink_off_() {
this->oe_pin_->digital_write(false);
this->gmod_pin_->digital_write(false);
GPIO.out &= ~(this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin()));
GPIO.out_w1tc = this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin());
this->ckv_pin_->digital_write(false);
this->sph_pin_->digital_write(false);
this->spv_pin_->digital_write(false);
+14 -1
View File
@@ -4,6 +4,7 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import infrared, remote_receiver, remote_transmitter
from esphome.components.const import CONF_RECEIVER_FREQUENCY
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
@@ -19,6 +20,7 @@ CONFIG_SCHEMA = cv.All(
infrared.infrared_schema(IrRfProxy).extend(
{
cv.Optional(CONF_FREQUENCY, default=0): cv.frequency,
cv.Optional(CONF_RECEIVER_FREQUENCY): cv.frequency,
cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent
),
@@ -33,7 +35,14 @@ CONFIG_SCHEMA = cv.All(
def _final_validate(config: dict[str, Any]) -> None:
"""Validate that transmitters have a proper carrier duty cycle."""
# Only validate if this is an infrared (not RF) configuration with a transmitter
# receiver_frequency is only meaningful for receiver configurations
if CONF_RECEIVER_FREQUENCY in config and CONF_REMOTE_RECEIVER_ID not in config:
raise cv.Invalid(
f"'{CONF_RECEIVER_FREQUENCY}' can only be used with '{CONF_REMOTE_RECEIVER_ID}', "
"not with a transmitter"
)
# Only validate duty cycle if this is an infrared (not RF) configuration with a transmitter
if config.get(CONF_FREQUENCY, 0) != 0 or CONF_REMOTE_TRANSMITTER_ID not in config:
return
@@ -75,3 +84,7 @@ async def to_code(config: dict[str, Any]) -> None:
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
# Set receiver demodulation frequency if specified (metadata only, no hardware effect)
if CONF_RECEIVER_FREQUENCY in config:
cg.add(var.set_receiver_frequency(config[CONF_RECEIVER_FREQUENCY]))
@@ -22,6 +22,9 @@ class IrRfProxy : public infrared::Infrared {
/// Check if this is RF mode (non-zero frequency)
bool is_rf() const { return this->frequency_khz_ > 0; }
/// Set the receiver's hardware demodulation frequency in Hz (metadata only, does not affect hardware)
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
protected:
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
+5 -9
View File
@@ -2,7 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE
AUTO_LOAD = ["ld24xx"]
DEPENDENCIES = ["uart"]
@@ -12,7 +12,6 @@ MULTI_CONF = True
ld2450_ns = cg.esphome_ns.namespace("ld2450")
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
CONF_LD2450_ID = "ld2450_id"
@@ -23,11 +22,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_THROTTLE): cv.invalid(
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
),
cv.Optional(CONF_ON_DATA): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
}
),
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
@@ -54,5 +49,6 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_DATA, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_data_callback", [], conf
)
-8
View File
@@ -1,6 +1,5 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#ifdef USE_SENSOR
@@ -201,11 +200,4 @@ class LD2450Component : public Component, public uart::UARTDevice {
LazyCallbackManager<void()> data_callback_;
};
class LD2450DataTrigger : public Trigger<> {
public:
explicit LD2450DataTrigger(LD2450Component *parent) {
parent->add_on_data_callback([this]() { this->trigger(); });
}
};
} // namespace esphome::ld2450
-1
View File
@@ -14,7 +14,6 @@ class LibreTinyComponent:
supports_atomics: bool = False # True for Cortex-M4(F) with LDREX/STREX
CONF_LIBRETINY = "libretiny"
CONF_LOGLEVEL = "loglevel"
CONF_SDK_SILENT = "sdk_silent"
CONF_GPIO_RECOVER = "gpio_recover"
+2 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components.const import CONF_LIBRETINY
import esphome.config_validation as cv
from esphome.const import (
CONF_VERSION,
@@ -7,7 +8,7 @@ from esphome.const import (
ICON_CELLPHONE_ARROW_DOWN,
)
from .const import CONF_LIBRETINY, LTComponent
from .const import LTComponent
DEPENDENCIES = ["libretiny"]
+85 -2
View File
@@ -24,6 +24,7 @@ from esphome.const import (
CONF_ID,
CONF_INITIAL_STATE,
CONF_MQTT_ID,
CONF_NAME,
CONF_ON_STATE,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
@@ -38,9 +39,11 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_WHITE,
)
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
from esphome.types import ConfigType
from .automation import LIGHT_STATE_SCHEMA
from .effects import (
@@ -70,9 +73,19 @@ IS_PLATFORM_COMPONENT = True
DOMAIN = "light"
@dataclass
class EffectRef:
"""A pending effect name reference from a light action to validate."""
light_id: ID
effect_name: str
component_path: list[str | int] # path_context when the action was validated
@dataclass
class LightData:
gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr
effect_refs: list[EffectRef] = field(default_factory=list)
def _get_data() -> LightData:
@@ -115,6 +128,68 @@ def _get_or_create_gamma_table(gamma_correct):
return fwd_arr
def find_effect_index(effects: list, effect_name: str) -> int | None:
"""Find the 1-based index of an effect by name (case-insensitive).
Returns the 1-based index if found, or None if not found.
"""
effect_name_lower = effect_name.lower()
for i, effect_conf in enumerate(effects):
key = next(iter(effect_conf))
if effect_conf[key][CONF_NAME].lower() == effect_name_lower:
return i + 1
return None
def available_effects_str(effects: list) -> str:
"""Return a comma-separated string of available effect names."""
available = [
effect_conf[next(iter(effect_conf))][CONF_NAME] for effect_conf in effects
]
return ", ".join(f"'{name}'" for name in available) if available else "none"
def _final_validate(config: ConfigType) -> ConfigType:
"""Validate all recorded effect name references against their target lights.
This runs once per light platform instance. If no light platform is configured,
this never runs — but the ID validator will catch the missing light ID separately.
"""
data = _get_data()
if not data.effect_refs:
return config
# Drain the list so we only validate once even though
# FINAL_VALIDATE_SCHEMA runs for each light platform instance.
refs = data.effect_refs
data.effect_refs = []
fconf = fv.full_config.get()
for ref in refs:
try:
light_path = fconf.get_path_for_id(ref.light_id)[:-1]
light_config = fconf.get_config_for_path(light_path)
except KeyError:
# Light ID not found — ID validation will have already reported this
continue
effects = light_config.get(CONF_EFFECTS, [])
if find_effect_index(effects, ref.effect_name) is None:
raise cv.FinalExternalInvalid(
f"Effect '{ref.effect_name}' not found for light "
f"'{ref.light_id}'. "
f"Available effects: {available_effects_str(effects)}",
path=[cv.ROOT_CONFIG_PATH] + ref.component_path,
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
LightRestoreMode = light_ns.enum("LightRestoreMode")
RESTORE_MODES = {
"RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF,
@@ -262,6 +337,8 @@ async def setup_light_core_(light_var, config, output_var):
cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))
if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None:
# Emit a stateless lambda that constructs the initial state — values live
# in flash as code, not stored in the LightState object (~40 bytes saved).
initial_state = LightStateRTCState(
initial_state_config.get(CONF_COLOR_MODE, ColorMode.UNKNOWN),
initial_state_config.get(CONF_STATE, False),
@@ -275,7 +352,13 @@ async def setup_light_core_(light_var, config, output_var):
initial_state_config.get(CONF_COLD_WHITE, 1.0),
initial_state_config.get(CONF_WARM_WHITE, 1.0),
)
cg.add(light_var.set_initial_state(initial_state))
args = [(LightStateRTCState.operator("ref"), "s")]
lamb = await cg.process_lambda(
Lambda(f"s = {initial_state};"),
args,
return_type=cg.void,
)
cg.add(light_var.set_initial_state(lamb))
if (
default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH)
+40 -9
View File
@@ -1,5 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.config import path_context
import esphome.config_validation as cv
from esphome.const import (
CONF_BLUE,
@@ -17,7 +18,6 @@ from esphome.const import (
CONF_LIMIT_MODE,
CONF_MAX_BRIGHTNESS,
CONF_MIN_BRIGHTNESS,
CONF_NAME,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_RED,
@@ -26,7 +26,7 @@ from esphome.const import (
CONF_WARM_WHITE,
CONF_WHITE,
)
from esphome.core import CORE, Lambda
from esphome.core import CORE, EsphomeError, Lambda
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
@@ -98,6 +98,31 @@ LIGHT_CONTROL_ACTION_SCHEMA = LIGHT_STATE_SCHEMA.extend(
}
)
def _record_effect_ref(config: ConfigType) -> ConfigType:
"""Record a static effect name reference for later cross-component validation."""
if CONF_EFFECT not in config:
return config
effect = config[CONF_EFFECT]
if isinstance(effect, Lambda):
return config # Lambda effects resolved at runtime
if effect.lower() == "none":
return config # "None" is always valid
from . import EffectRef, _get_data
_get_data().effect_refs.append(
EffectRef(
light_id=config[CONF_ID],
effect_name=effect,
component_path=path_context.get(),
)
)
return config
LIGHT_CONTROL_ACTION_SCHEMA.add_extra(_record_effect_ref)
LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
@@ -122,18 +147,24 @@ def _resolve_effect_index(config: ConfigType) -> int:
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
the C++ convention in LightState.
"""
from . import available_effects_str, find_effect_index
original_name = config[CONF_EFFECT]
effect_name = original_name.lower()
if effect_name == "none":
if original_name.lower() == "none":
return 0
light_id = config[CONF_ID]
light_path = CORE.config.get_path_for_id(light_id)[:-1]
light_config = CORE.config.get_config_for_path(light_path)
for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])):
key = next(iter(effect_conf))
if effect_conf[key][CONF_NAME].lower() == effect_name:
return i + 1
raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'")
effects = light_config.get(CONF_EFFECTS, [])
index = find_effect_index(effects, original_name)
if index is not None:
return index
# Should never reach here — effect names are validated during config
# validation in FINAL_VALIDATE_SCHEMA. This is a safety net.
raise EsphomeError(
f"Effect '{original_name}' not found for light '{light_id}'. "
f"Available effects: {available_effects_str(effects)}"
)
@automation.register_action(
+15 -15
View File
@@ -62,9 +62,9 @@ static const LogString *color_mode_to_human(ColorMode color_mode) {
}
// Helper to log percentage values
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static void log_percent(const LogString *param, float value) {
ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f);
ESP_LOGV(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f);
}
#else
#define log_percent(param, value)
@@ -76,20 +76,20 @@ void LightCall::perform() {
const bool publish = this->get_publish_();
if (publish) {
ESP_LOGD(TAG, "'%s' Setting:", name);
ESP_LOGV(TAG, "'%s' Setting:", name);
// Only print color mode when it's being changed
ColorMode current_color_mode = this->parent_->remote_values.get_color_mode();
ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode;
if (target_color_mode != current_color_mode) {
ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode())));
ESP_LOGV(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode())));
}
// Only print state when it's being changed
bool current_state = this->parent_->remote_values.is_on();
bool target_state = this->has_state() ? this->state_ : current_state;
if (target_state != current_state) {
ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on()));
ESP_LOGV(TAG, " State: %s", ONOFF(v.is_on()));
}
if (this->has_brightness()) {
@@ -100,7 +100,7 @@ void LightCall::perform() {
log_percent(LOG_STR("Color brightness"), v.get_color_brightness());
}
if (this->has_red() || this->has_green() || this->has_blue()) {
ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
ESP_LOGV(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
v.get_blue() * 100.0f);
}
@@ -108,11 +108,11 @@ void LightCall::perform() {
log_percent(LOG_STR("White"), v.get_white());
}
if (this->has_color_temperature()) {
ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature());
ESP_LOGV(TAG, " Color temperature: %.1f mireds", v.get_color_temperature());
}
if (this->has_cold_white() || this->has_warm_white()) {
ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f,
ESP_LOGV(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f,
v.get_warm_white() * 100.0f);
}
}
@@ -120,20 +120,20 @@ void LightCall::perform() {
if (this->has_flash_()) {
// FLASH
if (publish) {
ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f);
ESP_LOGV(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f);
}
this->parent_->start_flash_(v, this->flash_length_, publish);
} else if (this->has_transition_()) {
// TRANSITION
if (publish) {
ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f);
ESP_LOGV(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f);
}
// Special case: Transition and effect can be set when turning off
if (this->has_effect_()) {
if (publish) {
ESP_LOGD(TAG, " Effect: 'None'");
ESP_LOGV(TAG, " Effect: 'None'");
}
this->parent_->stop_effect_();
}
@@ -150,7 +150,7 @@ void LightCall::perform() {
}
if (publish) {
ESP_LOGD(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str());
ESP_LOGV(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str());
}
this->parent_->start_effect_(this->effect_);
@@ -385,7 +385,7 @@ void LightCall::transform_parameters_() {
!(this->color_mode_ & ColorCapability::WHITE) && //
!(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && //
min_mireds > 0.0f && max_mireds > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
ESP_LOGV(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str());
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
// This is important for state restoration, where both color_temperature and cold_white/warm_white
@@ -432,7 +432,7 @@ ColorMode LightCall::compute_color_mode_() {
// Don't change if the current mode is in the intersection (suitable AND supported)
if (ColorModeMask::mask_contains(intersection, current_mode)) {
ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
ESP_LOGV(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(current_mode)));
return current_mode;
}
@@ -440,7 +440,7 @@ ColorMode LightCall::compute_color_mode_() {
// Use the preferred suitable mode.
if (intersection != 0) {
ColorMode mode = ColorModeMask::first_value_from_mask(intersection);
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
ESP_LOGV(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(mode)));
return mode;
}

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