mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Merge remote-tracking branch 'origin/dev' into HEAD
# Conflicts: # esphome/components/mqtt/mqtt_button.cpp # esphome/components/mqtt/mqtt_lock.cpp # esphome/components/mqtt/mqtt_switch.cpp # esphome/components/mqtt/mqtt_update.cpp
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.22.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+25
-14
@@ -264,13 +264,17 @@ def string_ref_literal(config: ConfigType, value: str) -> str:
|
||||
return f"StringRef({literal_with_length(config, value)})"
|
||||
|
||||
|
||||
def _names_parent(text: str) -> bool:
|
||||
return any(f == "parent" for _, f, _, _ in string.Formatter().parse(text))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyCall:
|
||||
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
|
||||
|
||||
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
|
||||
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
|
||||
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
|
||||
path into nested sections. ``target`` and a plain ``str`` ``type_`` (raw C++ type text) may
|
||||
name the parent object as ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
|
||||
id bypasses it. The statement is skipped when none of its keys is set, always emitted when it
|
||||
has no keys, and a partial set is a config error.
|
||||
"""
|
||||
@@ -282,13 +286,13 @@ class ApplyCall:
|
||||
fields = [
|
||||
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
|
||||
]
|
||||
if any(fields):
|
||||
if any(f not in ("", "parent") for f in fields):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r}: only bare {{}} placeholders"
|
||||
f"apply target {self.target!r}: only {{}} and {{parent}} placeholders"
|
||||
)
|
||||
if len(fields) != len(self.args):
|
||||
if (count := fields.count("")) != len(self.args):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r} has {len(fields)} "
|
||||
f"apply target {self.target!r} has {count} "
|
||||
f"placeholder(s) for {len(self.args)} config key(s)"
|
||||
)
|
||||
if any(len(arg) not in (2, 3) for arg in self.args):
|
||||
@@ -296,6 +300,12 @@ class ApplyCall:
|
||||
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
|
||||
)
|
||||
|
||||
@property
|
||||
def names_parent(self) -> bool:
|
||||
return _names_parent(self.target) or any(
|
||||
isinstance(arg[1], str) and _names_parent(arg[1]) for arg in self.args
|
||||
)
|
||||
|
||||
@property
|
||||
def members(self) -> list[tuple[Any, Any, Any]]:
|
||||
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
|
||||
@@ -494,7 +504,7 @@ def register_apply_action(
|
||||
exprs = await _render_values(
|
||||
name, target, members, values, config, parent, lambda_args
|
||||
)
|
||||
statements.append(f"{receiver}{target.format(*exprs)};")
|
||||
statements.append(f"{receiver}{target.format(*exprs, parent=parent)};")
|
||||
if call:
|
||||
statements = [
|
||||
f"auto apply_call = {parent}->{call}();",
|
||||
@@ -526,7 +536,7 @@ async def _render_check(
|
||||
exprs = await _render_values(
|
||||
name, target, members, values, config, parent, lambda_args, compare=True
|
||||
)
|
||||
return target.format(*exprs)
|
||||
return target.format(*exprs, parent=parent)
|
||||
|
||||
|
||||
def register_apply_condition(
|
||||
@@ -1061,21 +1071,22 @@ async def build_trigger_callback(
|
||||
params: TemplateArgsType,
|
||||
forward: Sequence[str | Expression] | None = None,
|
||||
when: str | ApplyCall | None = None,
|
||||
parent: MockObj | None = None,
|
||||
) -> LambdaExpression:
|
||||
"""Build the Automation for ``config`` and return a stateless callback that triggers it.
|
||||
|
||||
``params`` are the parent callback's parameters, ``forward`` the expressions passed to
|
||||
``trigger()`` (default: the parameter names; write the parent as ``parent_ref(var)``),
|
||||
``when`` a filter the callback returns early on, skipped like any ``ApplyCall`` when none
|
||||
of its keys is set.
|
||||
of its keys is set. ``when`` may name ``parent`` as ``{parent}``, e.g.
|
||||
``"{parent}->is_fully_open()"``.
|
||||
"""
|
||||
members: list[tuple[Any, Any, Any]] = []
|
||||
if when is not None:
|
||||
call = when if isinstance(when, ApplyCall) else ApplyCall(when)
|
||||
members = call.members
|
||||
# A trigger callback has no parent for a str type to name.
|
||||
if any(isinstance(t, str) and "{parent}" in t for _, t, _ in members):
|
||||
raise ValueError(f"trigger filter {call.target!r}: a type names {{parent}}")
|
||||
if parent is None and call.names_parent:
|
||||
raise ValueError(f"trigger filter {call.target!r} names {{parent}}")
|
||||
obj = await _new_automation(args, config)
|
||||
lambda_args = _apply_lambda_args(params)
|
||||
statements: list[str] = []
|
||||
@@ -1088,7 +1099,7 @@ async def build_trigger_callback(
|
||||
members,
|
||||
values,
|
||||
config,
|
||||
None,
|
||||
None if parent is None else str(parent_ref(parent)),
|
||||
lambda_args,
|
||||
)
|
||||
statements.append(f"if (!({check}))\n return;")
|
||||
@@ -1137,7 +1148,7 @@ async def build_callback_automation(
|
||||
"forwarder cannot be combined with params, forward or when"
|
||||
)
|
||||
callback = await build_trigger_callback(
|
||||
args, config, args if params is None else params, forward, when
|
||||
args, config, args if params is None else params, forward, when, parent
|
||||
)
|
||||
cg.add(getattr(parent, callback_method)(callback))
|
||||
return
|
||||
|
||||
@@ -425,15 +425,24 @@ project({CORE.name})
|
||||
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||
# this command runs inside the link edge, blocking everything downstream.
|
||||
# The map is a BYPRODUCT so ninja knows the link writes it; IDF's size
|
||||
# target depends on the map and can then be built in the same run as all.
|
||||
# IDF's cmakev2 declares the map itself, so drop this line on that switch.
|
||||
# IDF's size target depends on the map, so ninja has to know the link
|
||||
# writes it for size to build in the same run as all. IDF declares the
|
||||
# map itself since espressif/esp-idf#19201 (and in cmakev2), and ninja
|
||||
# rejects two rules for one output, so declare it here only when IDF has
|
||||
# not. add_custom_command(OUTPUT) marks its outputs GENERATED.
|
||||
get_source_file_property(esphome_map_declared
|
||||
${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map GENERATED)
|
||||
if(esphome_map_declared)
|
||||
set(esphome_map_byproducts)
|
||||
else()
|
||||
set(esphome_map_byproducts BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map)
|
||||
endif()
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map
|
||||
${{esphome_map_byproducts}}
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
extern_progmem_array,
|
||||
get_variable,
|
||||
get_variable_with_full_id,
|
||||
is_static_pointer,
|
||||
is_template,
|
||||
new_Pvariable,
|
||||
new_variable,
|
||||
|
||||
@@ -760,7 +760,7 @@ message ListEntitiesLightResponse {
|
||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||
float min_mireds = 9;
|
||||
float max_mireds = 10;
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "light::LightEffectNames"];
|
||||
bool disabled_by_default = 13;
|
||||
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 15;
|
||||
|
||||
@@ -644,16 +644,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
msg.max_mireds = traits.get_max_mireds();
|
||||
}
|
||||
FixedVector<const char *> effects_list;
|
||||
if (light->supports_effects()) {
|
||||
auto &light_effects = light->get_effects();
|
||||
effects_list.init(light_effects.size() + 1);
|
||||
effects_list.push_back("None");
|
||||
for (auto *effect : light_effects) {
|
||||
// c_str() is safe as effect names are null-terminated strings from codegen
|
||||
effects_list.push_back(effect->get_name().c_str());
|
||||
}
|
||||
}
|
||||
light::LightEffectNames effects_list(light->get_effects());
|
||||
msg.effects = &effects_list;
|
||||
return fill_and_encode_entity_info(light, msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -688,8 +688,8 @@ uint8_t *ListEntitiesLightResponse::encode_msg(const void *self, uint8_t *__rest
|
||||
if (uint32_t raw = float_to_raw(msg.max_mireds); raw != 0) [[likely]] {
|
||||
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 85, raw);
|
||||
}
|
||||
for (const char *it : *msg.effects) {
|
||||
pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it, strlen(it));
|
||||
for (const auto &it : *msg.effects) {
|
||||
pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it);
|
||||
}
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.disabled_by_default);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
@@ -713,8 +713,8 @@ uint32_t ListEntitiesLightResponse::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_float(1, msg.min_mireds);
|
||||
size += ProtoSize::calc_float(1, msg.max_mireds);
|
||||
if (!msg.effects->empty()) {
|
||||
for (const char *it : *msg.effects) {
|
||||
size += ProtoSize::calc_length_force(1, strlen(it));
|
||||
for (const auto &it : *msg.effects) {
|
||||
size += ProtoSize::calc_length_force(1, it.size());
|
||||
}
|
||||
}
|
||||
size += ProtoSize::calc_bool(1, msg.disabled_by_default);
|
||||
|
||||
@@ -1059,7 +1059,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
|
||||
const light::ColorModeMask *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
const FixedVector<const char *> *effects{};
|
||||
const light::LightEffectNames *effects{};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
#include "esphome/components/light/light_effect_names.h"
|
||||
#include "esphome/components/light/light_traits.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -36,19 +36,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _subtree_references_uart(node: object, uart_id: str) -> bool:
|
||||
"""Return True if any dict in the subtree has a uart_id entry naming this bus."""
|
||||
if isinstance(node, dict):
|
||||
return any(
|
||||
(key == CONF_UART_ID and str(value) == uart_id)
|
||||
or _subtree_references_uart(value, uart_id)
|
||||
for key, value in node.items()
|
||||
)
|
||||
if isinstance(node, list):
|
||||
return any(_subtree_references_uart(item, uart_id) for item in node)
|
||||
return False
|
||||
|
||||
|
||||
def _reject_debug(uart_conf: ConfigType) -> ConfigType:
|
||||
# The worker tasks use the IDF driver directly, so the uart debugger never sees
|
||||
# bridge traffic and its dummy_receiver would drain RX bytes on the main loop.
|
||||
@@ -86,7 +73,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
|
||||
for domain, domain_conf in full_config.items():
|
||||
if domain == BRIDGE_DOMAIN:
|
||||
continue
|
||||
if _subtree_references_uart(domain_conf, owned_id):
|
||||
if uart.subtree_references_uart(domain_conf, owned_id):
|
||||
raise cv.Invalid(
|
||||
f"The {label} '{owned_id}' is also used by '{domain}'; a bridge "
|
||||
f"requires exclusive use of its {label}.",
|
||||
|
||||
@@ -27,6 +27,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_HOLD_STATE = "hold_state"
|
||||
CONF_HOST = "host"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_IAQ = "iaq"
|
||||
|
||||
@@ -148,7 +148,7 @@ void CS5460AComponent::schedule_next_check_() {
|
||||
*/
|
||||
if (time_left > -15000) {
|
||||
time_left = 1000;
|
||||
this->status_momentary_warning("warning", 1000);
|
||||
this->status_momentary_warning(1000);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Device officially stuck, resetting");
|
||||
this->cancel_timeout(STATUS_CHECK_TIMEOUT_ID);
|
||||
@@ -179,67 +179,67 @@ bool CS5460AComponent::check_status_() {
|
||||
if (status & (1 << 2)) {
|
||||
clear |= 1 << 2;
|
||||
ESP_LOGE(TAG, "Low supply detected");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 3)) {
|
||||
clear |= 1 << 3;
|
||||
ESP_LOGE(TAG, "Modulator oscillation on current channel");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 4)) {
|
||||
clear |= 1 << 4;
|
||||
ESP_LOGE(TAG, "Modulator oscillation on voltage channel");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 5)) {
|
||||
clear |= 1 << 5;
|
||||
ESP_LOGE(TAG, "Watch-dog timeout");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 11)) {
|
||||
clear |= 1 << 11;
|
||||
ESP_LOGE(TAG, "EOUT Energy Accumulation Register out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 12)) {
|
||||
clear |= 1 << 12;
|
||||
ESP_LOGE(TAG, "Energy out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 13)) {
|
||||
clear |= 1 << 13;
|
||||
ESP_LOGE(TAG, "RMS voltage out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 14)) {
|
||||
clear |= 1 << 14;
|
||||
ESP_LOGE(TAG, "RMS current out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 15)) {
|
||||
clear |= 1 << 15;
|
||||
ESP_LOGE(TAG, "Power calculation out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 16)) {
|
||||
clear |= 1 << 16;
|
||||
ESP_LOGE(TAG, "Voltage out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 17)) {
|
||||
clear |= 1 << 17;
|
||||
ESP_LOGE(TAG, "Current out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 19)) {
|
||||
|
||||
@@ -43,9 +43,6 @@ DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
|
||||
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
|
||||
DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase)
|
||||
|
||||
DateTimeStateTrigger = datetime_ns.class_(
|
||||
"DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
|
||||
)
|
||||
|
||||
OnTimeTrigger = datetime_ns.class_(
|
||||
"OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity)
|
||||
@@ -75,11 +72,7 @@ def _validate_time_present(config: ConfigType) -> ConfigType:
|
||||
_DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
)
|
||||
@@ -144,9 +137,16 @@ async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
# The state callback carries nothing; the automation gets the value as an ESPTime.
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_state_callback",
|
||||
[(cg.ESPTime, "x")],
|
||||
conf,
|
||||
params=[],
|
||||
forward=[automation.parent_ref(var).state_as_esptime()],
|
||||
)
|
||||
|
||||
if CONF_TIME_ID in config:
|
||||
rtc = await cg.get_variable(config[CONF_TIME_ID])
|
||||
|
||||
@@ -31,14 +31,4 @@ class DateTimeBase : public EntityBase {
|
||||
#endif
|
||||
};
|
||||
|
||||
class DateTimeStateTrigger final : public Trigger<ESPTime> {
|
||||
public:
|
||||
explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); });
|
||||
}
|
||||
|
||||
protected:
|
||||
DateTimeBase *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::datetime
|
||||
|
||||
@@ -58,7 +58,7 @@ void DebugComponent::loop() {
|
||||
if (new_free_heap < this->free_heap_ / 2) {
|
||||
this->free_heap_ = new_free_heap;
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
this->status_momentary_warning("heap", 1000);
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <cinttypes>
|
||||
#include <climits>
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -400,18 +401,17 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
auto uicr = [](volatile uint32_t *data, uint8_t size) {
|
||||
std::string res;
|
||||
// 13 words of 8 hex digits, separated by spaces
|
||||
char uicr_buf[13 * 9];
|
||||
auto uicr = [&uicr_buf](volatile uint32_t *data, uint8_t size) {
|
||||
size_t len = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
if (i > 0) {
|
||||
res += ' ';
|
||||
}
|
||||
res += format_hex_pretty<uint32_t>(data[i], '\0', false);
|
||||
len = buf_append_printf(uicr_buf, sizeof(uicr_buf), len, i > 0 ? " %08" PRIX32 : "%08" PRIX32, data[i]);
|
||||
}
|
||||
return res;
|
||||
return uicr_buf;
|
||||
};
|
||||
ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
|
||||
ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
|
||||
ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13));
|
||||
ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12));
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
log_peripherals_info();
|
||||
#endif
|
||||
|
||||
@@ -111,7 +111,17 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
|
||||
// Single pin wakeup (ext0) - ESP32, S2, S3 only
|
||||
#ifndef SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// Variants without per-pin deep sleep hold need the global enable
|
||||
gpio_deep_sleep_hold_en();
|
||||
#else
|
||||
if (this->wakeup_pin_ != nullptr)
|
||||
gpio_deep_sleep_hold_en();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Single pin wakeup (ext0) - ESP32, S2, S3 only
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
@@ -124,11 +134,6 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
}
|
||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||
gpio_hold_en(gpio_pin);
|
||||
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
||||
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
|
||||
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
|
||||
gpio_deep_sleep_hold_en();
|
||||
#endif
|
||||
bool level = !this->wakeup_pin_->is_inverted();
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
|
||||
@@ -33,7 +33,7 @@ class DemoAlarmControlPanel final : public AlarmControlPanel, public Component {
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
if (this->get_requires_code_to_arm()) {
|
||||
if (!code.has_value() || *code != "1234") {
|
||||
this->status_momentary_error("invalid_code", 5000);
|
||||
this->status_momentary_error(5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class DemoAlarmControlPanel final : public AlarmControlPanel, public Component {
|
||||
case ACP_STATE_DISARMED:
|
||||
if (this->get_requires_code()) {
|
||||
if (!code.has_value() || *code != "1234") {
|
||||
this->status_momentary_error("invalid_code", 5000);
|
||||
this->status_momentary_error(5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ from esphome.const import (
|
||||
CONF_ON_VALUE,
|
||||
CONF_SWITCH,
|
||||
CONF_TEXT,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@numo68"]
|
||||
DOMAIN = "display_menu_base"
|
||||
@@ -86,26 +87,6 @@ MENU_MODES = {
|
||||
CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK,
|
||||
}
|
||||
|
||||
DisplayMenuOnEnterTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnEnterTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnLeaveTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnValueTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnValueTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnNextTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnNextTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnPrevTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
|
||||
def validate_format(format):
|
||||
if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None:
|
||||
@@ -129,44 +110,20 @@ MENU_ITEM_COMMON_SCHEMA = cv.Schema(
|
||||
|
||||
MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnEnterTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnLeaveTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnValueTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnValueTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -230,20 +187,8 @@ MENU_ITEM_SCHEMA = cv.typed_schema(
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom),
|
||||
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_ON_NEXT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnNextTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PREV): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnPrevTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_NEXT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PREV): automation.validate_automation({}),
|
||||
}
|
||||
),
|
||||
},
|
||||
@@ -256,20 +201,8 @@ DISPLAY_MENU_BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
||||
cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu),
|
||||
cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES),
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnEnterTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnLeaveTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}),
|
||||
cv.Required(CONF_ITEMS): cv.All(
|
||||
cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1)
|
||||
),
|
||||
@@ -302,6 +235,24 @@ automation.register_apply_condition(
|
||||
)
|
||||
|
||||
|
||||
# Each item callback carries nothing; the automation receives the item itself.
|
||||
_ITEM_CALLBACKS = (
|
||||
(CONF_ON_ENTER, "add_on_enter_callback"),
|
||||
(CONF_ON_LEAVE, "add_on_leave_callback"),
|
||||
(CONF_ON_VALUE, "add_on_value_callback"),
|
||||
(CONF_ON_NEXT, "add_on_next_callback"),
|
||||
(CONF_ON_PREV, "add_on_prev_callback"),
|
||||
)
|
||||
|
||||
|
||||
async def _build_item_automations(item: MockObj, config: ConfigType) -> None:
|
||||
for conf_key, callback_method in _ITEM_CALLBACKS:
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_parent_callback_automation(
|
||||
item, callback_method, (MenuItemConstPtr, "it"), conf
|
||||
)
|
||||
|
||||
|
||||
async def menu_item_to_code(menu, config, parent):
|
||||
if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES:
|
||||
item = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -340,21 +291,7 @@ async def menu_item_to_code(menu, config, parent):
|
||||
cg.add(item.set_switch_variable(var))
|
||||
cg.add(item.set_on_text(config[CONF_ON_TEXT]))
|
||||
cg.add(item.set_off_text(config[CONF_OFF_TEXT]))
|
||||
for conf in config.get(CONF_ON_ENTER, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_LEAVE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_NEXT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_PREV, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
await _build_item_automations(item, config)
|
||||
|
||||
|
||||
async def display_menu_to_code(menu, config):
|
||||
@@ -364,9 +301,4 @@ async def display_menu_to_code(menu, config):
|
||||
cg.add(menu.set_mode(config[CONF_MODE]))
|
||||
for c in config[CONF_ITEMS]:
|
||||
await menu_item_to_code(menu, c, root_item)
|
||||
for conf in config.get(CONF_ON_ENTER, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_LEAVE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
await _build_item_automations(root_item, config)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "display_menu_base.h"
|
||||
|
||||
namespace esphome::display_menu_base {
|
||||
|
||||
class DisplayMenuOnEnterTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_enter_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnLeaveTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_leave_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnValueTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_value_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnNextTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_next_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnPrevTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_prev_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::display_menu_base
|
||||
@@ -3,9 +3,6 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, uart
|
||||
from esphome.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from esphome.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from esphome.components.text_sensor import DOMAIN as TEXT_SENSOR_DOMAIN
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
@@ -15,6 +12,7 @@ from esphome.const import (
|
||||
CONF_RECEIVE_TIMEOUT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"]
|
||||
@@ -30,19 +28,17 @@ CONF_SKIP_CRC = "skip_crc"
|
||||
CONF_DEFAULT_OBIS = "default_obis"
|
||||
CONF_PROVIDER = "provider"
|
||||
|
||||
DLMS_PARSER_VERSION = "2.2.0"
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
"DlmsMeterComponent", cg.Component, uart.UARTDevice
|
||||
)
|
||||
CustomPattern = dlms_meter_component_ns.struct("CustomPattern")
|
||||
ObisId = cg.global_ns.namespace("dlms_parser").class_("ObisId")
|
||||
|
||||
|
||||
def obis_code(value: Any) -> str:
|
||||
# Normalize the OBIS code to the strict A.B.C.D.E.F format
|
||||
bytes_list = parse_obis_code_bytes(value)
|
||||
return ".".join(str(b) for b in bytes_list)
|
||||
|
||||
|
||||
def parse_obis_code_bytes(value: Any) -> list[int]:
|
||||
def obis_string_to_byte_list(value: Any) -> list[int]:
|
||||
value = cv.string(value)
|
||||
normalized = re.sub(r"[\-\:\*]", ".", value)
|
||||
parts = normalized.split(".")
|
||||
@@ -60,29 +56,48 @@ def parse_obis_code_bytes(value: Any) -> list[int]:
|
||||
return bytes_list
|
||||
|
||||
|
||||
def to_obis_id_struct(value: list[int]) -> cg.Expression:
|
||||
return ObisId(*value)
|
||||
|
||||
|
||||
_request_sensor_slot = cg.slot_counter("DLMS_MAX_SENSORS")
|
||||
_request_text_sensor_slot = cg.slot_counter("DLMS_MAX_TEXT_SENSORS")
|
||||
_request_binary_sensor_slot = cg.slot_counter("DLMS_MAX_BINARY_SENSORS")
|
||||
|
||||
|
||||
def register_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None:
|
||||
_request_sensor_slot(str(hub))
|
||||
cg.add(hub.register_sensor(to_obis_id_struct(obis), var))
|
||||
|
||||
|
||||
def register_text_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None:
|
||||
_request_text_sensor_slot(str(hub))
|
||||
cg.add(hub.register_text_sensor(to_obis_id_struct(obis), var))
|
||||
|
||||
|
||||
def register_binary_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None:
|
||||
_request_binary_sensor_slot(str(hub))
|
||||
cg.add(hub.register_binary_sensor(to_obis_id_struct(obis), var))
|
||||
|
||||
|
||||
def custom_pattern_dict(value: Any) -> ConfigType:
|
||||
if isinstance(value, str):
|
||||
return {CONF_PATTERN: value}
|
||||
return value
|
||||
|
||||
|
||||
def validate_custom_pattern(value: ConfigType) -> ConfigType:
|
||||
if CONF_DEFAULT_OBIS in value and CONF_NAME not in value:
|
||||
raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set")
|
||||
return value
|
||||
|
||||
|
||||
CUSTOM_PATTERN_SCHEMA = cv.All(
|
||||
custom_pattern_dict,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATTERN): cv.string,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_NAME, default="CUSTOM"): cv.string,
|
||||
cv.Optional(CONF_PRIORITY, default=0): cv.int_,
|
||||
cv.Optional(CONF_DEFAULT_OBIS): parse_obis_code_bytes,
|
||||
cv.Optional(
|
||||
CONF_DEFAULT_OBIS, default="0.0.0.0.0.0"
|
||||
): obis_string_to_byte_list,
|
||||
}
|
||||
),
|
||||
validate_custom_pattern,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
@@ -120,74 +135,30 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
dec_key_expr = cg.RawExpression("std::nullopt")
|
||||
if dec_key := config.get(CONF_DECRYPTION_KEY):
|
||||
key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||
dec_key_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||
custom_patterns = [
|
||||
cg.StructInitializer(
|
||||
CustomPattern,
|
||||
(CONF_PATTERN, pattern[CONF_PATTERN]),
|
||||
(CONF_NAME, pattern[CONF_NAME]),
|
||||
(CONF_PRIORITY, pattern[CONF_PRIORITY]),
|
||||
(CONF_DEFAULT_OBIS, to_obis_id_struct(pattern[CONF_DEFAULT_OBIS])),
|
||||
)
|
||||
|
||||
auth_key_expr = cg.RawExpression("std::nullopt")
|
||||
if auth_key := config.get(CONF_AUTH_KEY):
|
||||
key_bytes = [str(int(auth_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||
auth_key_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||
)
|
||||
|
||||
patterns = []
|
||||
if custom_patterns := config.get(CONF_CUSTOM_PATTERNS):
|
||||
for p in custom_patterns:
|
||||
name_expr = cg.RawExpression("std::nullopt")
|
||||
if name_val := p.get(CONF_NAME):
|
||||
name_expr = name_val
|
||||
|
||||
if obis_vals := p.get(CONF_DEFAULT_OBIS):
|
||||
obis_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 6>{{{obis_vals[0]}, {obis_vals[1]}, {obis_vals[2]}, {obis_vals[3]}, {obis_vals[4]}, {obis_vals[5]}}}"
|
||||
)
|
||||
else:
|
||||
obis_expr = cg.RawExpression("std::nullopt")
|
||||
|
||||
patterns.append(
|
||||
cg.ArrayInitializer(
|
||||
p[CONF_PATTERN],
|
||||
name_expr,
|
||||
p[CONF_PRIORITY],
|
||||
obis_expr,
|
||||
)
|
||||
)
|
||||
|
||||
patterns_expr = (
|
||||
cg.ArrayInitializer(*patterns) if patterns else cg.RawExpression("{}")
|
||||
)
|
||||
for pattern in config.get(CONF_CUSTOM_PATTERNS, [])
|
||||
]
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_RECEIVE_TIMEOUT],
|
||||
config[CONF_SKIP_CRC],
|
||||
dec_key_expr,
|
||||
auth_key_expr,
|
||||
patterns_expr,
|
||||
config.get(CONF_DECRYPTION_KEY, cg.nullptr),
|
||||
config.get(CONF_AUTH_KEY, cg.nullptr),
|
||||
cg.ArrayInitializer(*custom_patterns, multiline=True),
|
||||
)
|
||||
|
||||
hub_id = config[CONF_ID].id
|
||||
|
||||
for domain, define in (
|
||||
(SENSOR_DOMAIN, "DLMS_MAX_SENSORS"),
|
||||
(TEXT_SENSOR_DOMAIN, "DLMS_MAX_TEXT_SENSORS"),
|
||||
(BINARY_SENSOR_DOMAIN, "DLMS_MAX_BINARY_SENSORS"),
|
||||
):
|
||||
count = sum(
|
||||
1
|
||||
for conf in CORE.config.get(domain, [])
|
||||
if conf.get("platform") == DOMAIN and conf[CONF_DLMS_METER_ID].id == hub_id
|
||||
)
|
||||
cg.add_define(define, count)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if CORE.is_esp32:
|
||||
esp32.add_idf_component(name="esphome/dlms_parser", ref="1.1.0")
|
||||
esp32.add_idf_component(name="esphome/dlms_parser", ref=DLMS_PARSER_VERSION)
|
||||
else:
|
||||
cg.add_library("esphome/dlms_parser", "1.1.0")
|
||||
cg.add_library("esphome/dlms_parser", DLMS_PARSER_VERSION)
|
||||
|
||||
@@ -3,14 +3,20 @@ from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
from .. import (
|
||||
CONF_DLMS_METER_ID,
|
||||
CONF_OBIS_CODE,
|
||||
DlmsMeterComponent,
|
||||
obis_string_to_byte_list,
|
||||
register_binary_sensor,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,4 +24,4 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var))
|
||||
register_binary_sensor(hub, config[CONF_OBIS_CODE], var)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Ignore Zephyr. It doesn't have any encryption library.
|
||||
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
|
||||
|
||||
#include "dlms_meter.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
@@ -31,54 +36,40 @@ static void log_callback(dlms_parser::LogLevel level, const char *fmt, va_list a
|
||||
}
|
||||
}
|
||||
|
||||
DlmsMeterComponent::DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||
DlmsMeterComponent::DlmsMeterComponent(const uint32_t receive_timeout_ms, const bool skip_crc_check,
|
||||
const char *decryption_key, const char *authentication_key,
|
||||
std::vector<CustomPattern> custom_patterns)
|
||||
: receive_timeout_ms_(receive_timeout_ms),
|
||||
skip_crc_check_(skip_crc_check),
|
||||
custom_patterns_(std::move(custom_patterns)),
|
||||
parser_(&decryptor_) {
|
||||
parser_([this](const dlms_parser::AxdrCapture &capture) { this->on_data_(capture); }, &this->decryptor_) {
|
||||
dlms_parser::Logger::set_log_function(log_callback);
|
||||
|
||||
if (decryption_key.has_value()) {
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
ESP_LOGE(TAG, "Decryption is not supported on this platform (no compatible crypto library found)");
|
||||
#else
|
||||
auto opt_key = dlms_parser::Aes128GcmDecryptionKey::from_bytes(decryption_key.value());
|
||||
if (opt_key) {
|
||||
this->parser_.set_decryption_key(*opt_key);
|
||||
if (decryption_key != nullptr) {
|
||||
const auto key = dlms_parser::Aes128GcmDecryptionKey::from_hex(decryption_key);
|
||||
if (key.has_value()) {
|
||||
this->parser_.set_decryption_key(*key);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set decryption key: invalid key format");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (authentication_key.has_value()) {
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
ESP_LOGE(TAG, "Authentication is not supported on this platform (no compatible crypto library found)");
|
||||
#else
|
||||
auto opt_key = dlms_parser::Aes128GcmAuthenticationKey::from_bytes(authentication_key.value());
|
||||
if (opt_key) {
|
||||
this->parser_.set_authentication_key(*opt_key);
|
||||
if (authentication_key != nullptr) {
|
||||
const auto key = dlms_parser::Aes128GcmAuthenticationKey::from_hex(authentication_key);
|
||||
if (key.has_value()) {
|
||||
this->parser_.set_authentication_key(*key);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set authentication key: invalid key format");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
this->parser_.set_skip_crc_check(this->skip_crc_check_);
|
||||
|
||||
this->parser_.load_default_patterns();
|
||||
for (const auto &pattern : this->custom_patterns_) {
|
||||
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority,
|
||||
pattern.default_obis.value());
|
||||
} else if (pattern.name.has_value()) {
|
||||
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority);
|
||||
} else {
|
||||
this->parser_.register_pattern(pattern.pattern.c_str());
|
||||
}
|
||||
|
||||
for (const auto &custom_pattern : this->custom_patterns_) {
|
||||
this->parser_.register_pattern(custom_pattern.name, custom_pattern.pattern, custom_pattern.priority,
|
||||
custom_pattern.default_obis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,36 +80,36 @@ void DlmsMeterComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Receive Timeout: %" PRIu32 " ms", this->receive_timeout_ms_);
|
||||
ESP_LOGCONFIG(TAG, " Skip CRC Check: %s", YESNO(this->skip_crc_check_));
|
||||
|
||||
for (const auto &pattern : this->custom_patterns_) {
|
||||
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||
const auto &obis = pattern.default_obis.value();
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %d.%d.%d.%d.%d.%d)",
|
||||
pattern.pattern.c_str(), pattern.name->c_str(), pattern.priority, obis[0], obis[1], obis[2],
|
||||
obis[3], obis[4], obis[5]);
|
||||
} else if (pattern.name.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d)", pattern.pattern.c_str(),
|
||||
pattern.name->c_str(), pattern.priority);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s'", pattern.pattern.c_str());
|
||||
}
|
||||
std::array<char, 24> obis_buf;
|
||||
|
||||
for (const auto &custom_pattern : this->custom_patterns_) {
|
||||
const auto default_obis = custom_pattern.default_obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %.*s)", custom_pattern.pattern,
|
||||
custom_pattern.name, custom_pattern.priority, static_cast<int>(default_obis.size()),
|
||||
default_obis.data());
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &entry : this->sensors_) {
|
||||
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
for (const auto &item : this->sensors_) {
|
||||
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", item.sensor);
|
||||
const auto obis = item.obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast<int>(obis.size()), obis.data());
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
for (const auto &entry : this->text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
for (const auto &item : this->text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", item.sensor);
|
||||
const auto obis = item.obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast<int>(obis.size()), obis.data());
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &entry : this->binary_sensors_) {
|
||||
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
for (const auto &item : this->binary_sensors_) {
|
||||
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", item.sensor);
|
||||
const auto obis = item.obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast<int>(obis.size()), obis.data());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -166,71 +157,73 @@ void DlmsMeterComponent::read_rx_buffer_() {
|
||||
void DlmsMeterComponent::process_frame_() {
|
||||
ESP_LOGV(TAG, "Processing frame of size: %zu bytes", this->bytes_accumulated_);
|
||||
|
||||
auto callback = [this](const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||
this->on_data_(obis_code, float_val, str_val, is_numeric);
|
||||
};
|
||||
|
||||
this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_}, callback);
|
||||
this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_});
|
||||
|
||||
this->bytes_accumulated_ = 0;
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||
void DlmsMeterComponent::on_data_(const dlms_parser::AxdrCapture &capture) {
|
||||
int updated_count = 0;
|
||||
|
||||
if (capture.is_numeric()) {
|
||||
#if defined(USE_SENSOR) || defined(USE_BINARY_SENSOR)
|
||||
const float value = capture.value_as_float_with_scaler_applied();
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (is_numeric) {
|
||||
for (auto &item : this->sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(float_val);
|
||||
for (const auto &item : this->sensors_) {
|
||||
if (item.obis == capture.obis) {
|
||||
item.sensor->publish_state(value);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (!is_numeric && str_val != nullptr) {
|
||||
for (auto &item : this->text_sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(str_val);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (is_numeric) {
|
||||
bool state = float_val != 0.0f;
|
||||
for (auto &item : this->binary_sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(state);
|
||||
for (const auto &item : this->binary_sensors_) {
|
||||
if (item.obis == capture.obis) {
|
||||
item.sensor->publish_state(value != 0.0f);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
for (const auto &item : this->text_sensors_) {
|
||||
if (item.obis == capture.obis) {
|
||||
std::array<char, 128> value_buf;
|
||||
const auto value = capture.value_as_string(value_buf);
|
||||
item.sensor->publish_state(value.data(), value.size());
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (updated_count == 0) {
|
||||
ESP_LOGV(TAG, "Received OBIS %s, but no sensors are registered for it.", obis_code);
|
||||
std::array<char, 24> obis_buf;
|
||||
const auto obis = capture.obis.to_string(obis_buf);
|
||||
ESP_LOGV(TAG, "Received OBIS %.*s, but no sensors are registered for it.", static_cast<int>(obis.size()),
|
||||
obis.data());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void DlmsMeterComponent::register_sensor(const std::string &obis_code, sensor::Sensor *sensor) {
|
||||
this->sensors_.push_back({obis_code, sensor});
|
||||
void DlmsMeterComponent::register_sensor(const dlms_parser::ObisId &obis, sensor::Sensor *sensor) {
|
||||
this->sensors_.push_back({obis, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void DlmsMeterComponent::register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor) {
|
||||
this->text_sensors_.push_back({obis_code, sensor});
|
||||
void DlmsMeterComponent::register_text_sensor(const dlms_parser::ObisId &obis, text_sensor::TextSensor *sensor) {
|
||||
this->text_sensors_.push_back({obis, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void DlmsMeterComponent::register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor) {
|
||||
this->binary_sensors_.push_back({obis_code, sensor});
|
||||
void DlmsMeterComponent::register_binary_sensor(const dlms_parser::ObisId &obis, binary_sensor::BinarySensor *sensor) {
|
||||
this->binary_sensors_.push_back({obis, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
// Ignore Zephyr. It doesn't have any encryption library.
|
||||
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -19,12 +23,6 @@
|
||||
|
||||
#include <dlms_parser/dlms_parser.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h>
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
@@ -35,9 +33,12 @@
|
||||
#elif __has_include(<bearssl/bearssl.h>)
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h>
|
||||
#else
|
||||
#define DLMS_METER_NO_CRYPTO
|
||||
#error "The platform doesn't provide a compatible encryption library for dlms_parser"
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
// Entity storage is sized by codegen; a platform with no entities emits no define
|
||||
#ifndef DLMS_MAX_SENSORS
|
||||
static constexpr uint8_t DLMS_MAX_SENSORS = 0;
|
||||
#endif
|
||||
@@ -48,82 +49,64 @@ static constexpr uint8_t DLMS_MAX_TEXT_SENSORS = 0;
|
||||
static constexpr uint8_t DLMS_MAX_BINARY_SENSORS = 0;
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
// Fallback dummy decryptor for platforms without supported crypto (e.g., Zephyr during clang-tidy)
|
||||
class Aes128GcmDecryptorDummy : public dlms_parser::Aes128GcmDecryptor {
|
||||
public:
|
||||
void set_decryption_key(const dlms_parser::Aes128GcmDecryptionKey &key) override {}
|
||||
bool decrypt_in_place(std::span<const uint8_t> iv, std::span<uint8_t> ciphertext_and_plaintext,
|
||||
std::span<const uint8_t> aad, std::span<const uint8_t> tag) override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa;
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls;
|
||||
#elif __has_include(<bearssl/bearssl.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl;
|
||||
#else
|
||||
using Aes128GcmDecryptorImpl = Aes128GcmDecryptorDummy;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
struct SensorItem {
|
||||
std::string obis_code;
|
||||
dlms_parser::ObisId obis;
|
||||
sensor::Sensor *sensor;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
struct TextSensorItem {
|
||||
std::string obis_code;
|
||||
dlms_parser::ObisId obis;
|
||||
text_sensor::TextSensor *sensor;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct BinarySensorItem {
|
||||
std::string obis_code;
|
||||
dlms_parser::ObisId obis;
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct CustomPattern {
|
||||
std::string pattern;
|
||||
std::optional<std::string> name;
|
||||
int priority{0};
|
||||
std::optional<std::array<uint8_t, 6>> default_obis;
|
||||
const char *pattern;
|
||||
const char *name;
|
||||
int priority;
|
||||
dlms_parser::ObisId default_obis;
|
||||
};
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa;
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls;
|
||||
#else
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl;
|
||||
#endif
|
||||
|
||||
class DlmsMeterComponent final : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||
std::vector<CustomPattern> custom_patterns);
|
||||
DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check, const char *decryption_key,
|
||||
const char *authentication_key, std::vector<CustomPattern> custom_patterns);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void register_sensor(const std::string &obis_code, sensor::Sensor *sensor);
|
||||
void register_sensor(const dlms_parser::ObisId &obis, sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor);
|
||||
void register_text_sensor(const dlms_parser::ObisId &obis, text_sensor::TextSensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor);
|
||||
void register_binary_sensor(const dlms_parser::ObisId &obis, binary_sensor::BinarySensor *sensor);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_rx_buffer_();
|
||||
void flush_rx_buffer_();
|
||||
void process_frame_();
|
||||
void on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric);
|
||||
void on_data_(const dlms_parser::AxdrCapture &capture);
|
||||
|
||||
std::array<uint8_t, 2048> rx_buffer_;
|
||||
size_t bytes_accumulated_{0};
|
||||
@@ -149,3 +132,5 @@ class DlmsMeterComponent final : public Component, public uart::UARTDevice {
|
||||
};
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
|
||||
#endif
|
||||
|
||||
@@ -3,7 +3,13 @@ from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
from .. import (
|
||||
CONF_DLMS_METER_ID,
|
||||
CONF_OBIS_CODE,
|
||||
DlmsMeterComponent,
|
||||
obis_string_to_byte_list,
|
||||
register_sensor,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
@@ -40,7 +46,7 @@ REMOVED_KEYS = {
|
||||
CONFIG_SCHEMA = sensor.sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list,
|
||||
**{
|
||||
cv.Optional(key): cv.invalid(
|
||||
f"The predefined '{key}' key was removed in ESPHome 2026.11.0. "
|
||||
@@ -56,4 +62,4 @@ CONFIG_SCHEMA = sensor.sensor_schema().extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
var = await sensor.new_sensor(config)
|
||||
cg.add(hub.register_sensor(config[CONF_OBIS_CODE], var))
|
||||
register_sensor(hub, config[CONF_OBIS_CODE], var)
|
||||
|
||||
@@ -3,7 +3,13 @@ from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
from .. import (
|
||||
CONF_DLMS_METER_ID,
|
||||
CONF_OBIS_CODE,
|
||||
DlmsMeterComponent,
|
||||
obis_string_to_byte_list,
|
||||
register_text_sensor,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
@@ -18,7 +24,7 @@ REMOVED_KEYS = {
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list,
|
||||
**{
|
||||
cv.Optional(key): cv.invalid(
|
||||
f"The predefined '{key}' key was removed in ESPHome 2026.11.0. "
|
||||
@@ -34,4 +40,4 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
cg.add(hub.register_text_sensor(config[CONF_OBIS_CODE], var))
|
||||
register_text_sensor(hub, config[CONF_OBIS_CODE], var)
|
||||
|
||||
@@ -231,7 +231,7 @@ bool ES8388::set_dac_output(DacOutputLine line) {
|
||||
optional<DacOutputLine> ES8388::get_dac_power() {
|
||||
uint8_t dac_power;
|
||||
if (!this->read_byte(ES8388_DACPOWER, &dac_power)) {
|
||||
this->status_momentary_warning("dacpower_read");
|
||||
this->status_momentary_warning();
|
||||
return {};
|
||||
}
|
||||
switch (dac_power) {
|
||||
@@ -274,7 +274,7 @@ bool ES8388::set_adc_input_mic(AdcInputMicLine line) {
|
||||
optional<AdcInputMicLine> ES8388::get_mic_input() {
|
||||
uint8_t mic_input;
|
||||
if (!this->read_byte(ES8388_ADCCONTROL2, &mic_input)) {
|
||||
this->status_momentary_warning("adccontrol2_read");
|
||||
this->status_momentary_warning();
|
||||
return {};
|
||||
}
|
||||
switch (mic_input) {
|
||||
|
||||
@@ -15,7 +15,6 @@ from esphome.components.const import (
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION,
|
||||
CONF_IGNORE_NOT_FOUND,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
@@ -3967,10 +3966,3 @@ def process_stacktrace(config, line, backtrace_state):
|
||||
_decode_pc(config, addr.group())
|
||||
|
||||
return backtrace_state
|
||||
|
||||
|
||||
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
|
||||
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"}
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "esphome/core/defines.h"
|
||||
// Also defines the core ISRInternalGPIOPin methods; those are only reachable
|
||||
// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely.
|
||||
#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO)
|
||||
// Always built: InternalGPIOPin is this class, so components that merely hold a
|
||||
// pointer to it reference these methods even in configs without a pin.
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/rtc_io.h"
|
||||
@@ -49,17 +50,52 @@ struct ISRPinArg {
|
||||
gpio_num_t pin;
|
||||
gpio::Flags flags;
|
||||
bool inverted;
|
||||
#ifdef USE_GPIO_HOLD
|
||||
bool hold;
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
bool use_rtc;
|
||||
int rtc_pin;
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// Re-latch a held pad onto the values just written to its registers.
|
||||
static inline void refresh_hold(const ESP32InternalGPIOPin &pin) {
|
||||
pin.set_hold(false);
|
||||
pin.set_hold(true);
|
||||
}
|
||||
|
||||
static inline void IRAM_ATTR isr_refresh_hold(const ISRPinArg *arg) {
|
||||
if (!arg->hold)
|
||||
return;
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
if (arg->use_rtc) {
|
||||
rtcio_hal_hold_disable(arg->rtc_pin);
|
||||
rtcio_hal_hold_enable(arg->rtc_pin);
|
||||
return;
|
||||
}
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// GPIO0-21 are held from the RTC domain; the digital hold mask is zero for them
|
||||
if (arg->pin < SOC_RTCIO_PIN_COUNT) {
|
||||
rtcio_hal_hold_disable(arg->pin);
|
||||
rtcio_hal_hold_enable(arg->pin);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
gpio_hal_hold_dis(&GPIO_HAL, arg->pin);
|
||||
gpio_hal_hold_en(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
#endif
|
||||
|
||||
ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
arg->pin = this->get_pin_num();
|
||||
arg->flags = gpio::FLAG_NONE;
|
||||
arg->inverted = this->pin_flags_.inverted;
|
||||
#ifdef USE_GPIO_HOLD
|
||||
arg->hold = this->get_hold();
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num());
|
||||
if (arg->use_rtc)
|
||||
@@ -68,7 +104,7 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void ESP32InternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE;
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
@@ -104,7 +140,23 @@ size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "GPIO%" PRIu32, static_cast<uint32_t>(this->pin_));
|
||||
}
|
||||
|
||||
#ifdef USE_GPIO_HOLD
|
||||
void ESP32InternalGPIOPin::set_hold(bool hold) const {
|
||||
if (!this->get_hold())
|
||||
return;
|
||||
if (hold) {
|
||||
gpio_hold_en(this->get_pin_num());
|
||||
} else {
|
||||
gpio_hold_dis(this->get_pin_num());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32InternalGPIOPin::setup() {
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// Hold before gpio_config so the pad keeps its state while the registers change
|
||||
this->set_hold(true);
|
||||
#endif
|
||||
gpio_config_t conf{};
|
||||
conf.pin_bit_mask = 1ULL << static_cast<uint32_t>(this->pin_);
|
||||
conf.mode = flags_to_mode(this->flags_);
|
||||
@@ -115,6 +167,18 @@ void ESP32InternalGPIOPin::setup() {
|
||||
if (this->flags_ & gpio::FLAG_OUTPUT) {
|
||||
gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength());
|
||||
}
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// gpio_hold_dis logs an error on input-only pads
|
||||
if (GPIO_IS_VALID_OUTPUT_GPIO(this->get_pin_num())) {
|
||||
if (!this->get_hold()) {
|
||||
// Release a hold left behind by an earlier boot
|
||||
gpio_hold_dis(this->get_pin_num());
|
||||
} else if (this->flags_ & gpio::FLAG_INPUT) {
|
||||
// Inputs apply the config now so reads work; outputs wait for the first write
|
||||
refresh_hold(*this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
@@ -129,6 +193,9 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
pull_mode = GPIO_PULLDOWN_ONLY;
|
||||
}
|
||||
gpio_set_pull_mode(this->get_pin_num(), pull_mode);
|
||||
#ifdef USE_GPIO_HOLD
|
||||
refresh_hold(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ESP32InternalGPIOPin::digital_read() {
|
||||
@@ -136,6 +203,9 @@ bool ESP32InternalGPIOPin::digital_read() {
|
||||
}
|
||||
void ESP32InternalGPIOPin::digital_write(bool value) {
|
||||
gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0);
|
||||
#ifdef USE_GPIO_HOLD
|
||||
refresh_hold(*this);
|
||||
#endif
|
||||
}
|
||||
void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); }
|
||||
|
||||
@@ -143,6 +213,8 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->ge
|
||||
|
||||
using namespace esp32;
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- some gpio_hal functions use MMIO at fixed addresses
|
||||
// internally
|
||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted;
|
||||
@@ -151,6 +223,9 @@ bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted);
|
||||
#ifdef USE_GPIO_HOLD
|
||||
isr_refresh_hold(arg);
|
||||
#endif
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
|
||||
@@ -202,9 +277,13 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
gpio_hal_input_disable(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
}
|
||||
#ifdef USE_GPIO_HOLD
|
||||
isr_refresh_hold(arg);
|
||||
#endif
|
||||
arg->flags = flags;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-core.FixedAddressDereference)
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
#include <esp_system.h>
|
||||
#include <driver/gpio.h>
|
||||
|
||||
namespace esphome::esp32 {
|
||||
@@ -10,7 +11,7 @@ namespace esphome::esp32 {
|
||||
static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t");
|
||||
static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field");
|
||||
|
||||
class ESP32InternalGPIOPin final : public InternalGPIOPin {
|
||||
class ESP32InternalGPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(gpio_num_t pin) { this->pin_ = static_cast<uint8_t>(pin); }
|
||||
void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; }
|
||||
@@ -24,16 +25,30 @@ class ESP32InternalGPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return this->pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return this->pin_; }
|
||||
gpio::Flags get_flags() const override { return this->flags_; }
|
||||
bool is_inverted() const override { return this->pin_flags_.inverted; }
|
||||
bool is_inverted() const { return this->pin_flags_.inverted; }
|
||||
bool is_internal() override { return true; }
|
||||
gpio_num_t get_pin_num() const { return static_cast<gpio_num_t>(this->pin_); }
|
||||
gpio_drive_cap_t get_drive_strength() const { return static_cast<gpio_drive_cap_t>(this->pin_flags_.drive_strength); }
|
||||
#ifdef USE_GPIO_HOLD
|
||||
/// Apply or release the pad hold. Does nothing unless the pin is configured with hold_state.
|
||||
void set_hold(bool hold) const;
|
||||
bool is_held() const override {
|
||||
if (!this->get_hold())
|
||||
return false;
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
return reason == ESP_RST_DEEPSLEEP || reason == ESP_RST_SW;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
// Memory layout: 8 bytes total on 32-bit systems
|
||||
// - 3 bytes for members below
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
@@ -248,6 +249,7 @@ ESP32_PIN_SCHEMA = cv.All(
|
||||
cv.float_with_unit("current", "mA", optional_unit=True),
|
||||
cv.enum(DRIVE_STRENGTHS),
|
||||
),
|
||||
cv.Optional(CONF_HOLD_STATE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
validate_gpio_pin,
|
||||
@@ -257,7 +259,6 @@ ESP32_PIN_SCHEMA = cv.All(
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
|
||||
async def esp32_pin_to_code(config):
|
||||
cg.add_define("USE_ESP32_INTERNAL_GPIO")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
||||
@@ -267,7 +268,11 @@ async def esp32_pin_to_code(config):
|
||||
cg.add(var.set_inverted(True))
|
||||
if CONF_DRIVE_STRENGTH in config:
|
||||
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
flags = pins.gpio_flags_expr(config[CONF_MODE])
|
||||
if config[CONF_HOLD_STATE]:
|
||||
flags = flags | cg.gpio_Flags.FLAG_HOLD
|
||||
cg.add_define("USE_GPIO_HOLD")
|
||||
cg.add(var.set_flags(flags))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Pin checks shared by the ESP32 variant validators."""
|
||||
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
from esphome.const import CONF_NUMBER
|
||||
|
||||
|
||||
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
|
||||
if num in 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.",
|
||||
num,
|
||||
)
|
||||
|
||||
|
||||
def check_usb_jtag_hold(
|
||||
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
|
||||
) -> None:
|
||||
num = conf[CONF_NUMBER]
|
||||
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
@@ -53,6 +54,7 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
is_output = mode[CONF_OUTPUT]
|
||||
is_pullup = mode[CONF_PULLUP]
|
||||
is_pulldown = mode[CONF_PULLDOWN]
|
||||
is_hold = value.get(CONF_HOLD_STATE)
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
@@ -70,6 +72,14 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
|
||||
)
|
||||
if is_hold and 34 <= num <= 39:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) is input-only and cannot be held.",
|
||||
[CONF_HOLD_STATE],
|
||||
)
|
||||
if is_hold and num == 20:
|
||||
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
|
||||
raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE])
|
||||
|
||||
check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32C3_SPI_PSRAM_PINS = {
|
||||
12: "SPIHD",
|
||||
13: "SPIWP",
|
||||
@@ -28,12 +30,7 @@ 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,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -49,6 +46,7 @@ def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
|
||||
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
|
||||
|
||||
@@ -20,6 +22,8 @@ _ESP32C5_SPI_PSRAM_PINS = {
|
||||
|
||||
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
|
||||
|
||||
_ESP32C5_USB_JTAG_PINS = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -31,6 +35,7 @@ def esp32_c5_validate_gpio_pin(value: int) -> int:
|
||||
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -45,6 +50,8 @@ def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
|
||||
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
|
||||
|
||||
@@ -32,12 +34,7 @@ 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,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -52,6 +49,7 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
|
||||
_ESP32C61_SPI_PSRAM_PINS = {
|
||||
14: "SPICS0",
|
||||
@@ -18,6 +20,8 @@ _ESP32C61_SPI_PSRAM_PINS = {
|
||||
|
||||
_ESP32C61_STRAPPING_PINS = {8, 9}
|
||||
|
||||
_ESP32C61_USB_JTAG_PINS = {12, 13}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -29,6 +33,7 @@ def esp32_c61_validate_gpio_pin(value: int) -> int:
|
||||
f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -43,5 +48,7 @@ def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32-C61 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
|
||||
|
||||
_ESP32H2_USB_JTAG_PINS = {26, 27}
|
||||
@@ -24,12 +26,7 @@ def esp32_h2_validate_gpio_pin(value: int) -> int:
|
||||
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html",
|
||||
value,
|
||||
)
|
||||
if value in _ESP32H2_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,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -44,5 +41,6 @@ def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
|
||||
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,17 +5,22 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
|
||||
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_ESP32H21_USB_JTAG_PINS = {17, 18}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h21_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 25:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
|
||||
check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -30,5 +35,7 @@ def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,17 +5,22 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
|
||||
_ESP32H4_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_ESP32H4_USB_JTAG_PINS = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
|
||||
check_usb_jtag_pin(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -30,5 +35,7 @@ def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf
|
||||
_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||
|
||||
@@ -18,12 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_p4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 54:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
|
||||
if value in _ESP32P4_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,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -38,6 +35,7 @@ def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
|
||||
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
@@ -70,6 +71,11 @@ def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
|
||||
)
|
||||
if value.get(CONF_HOLD_STATE) and num == 46:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} is input-only and cannot be held.",
|
||||
[CONF_HOLD_STATE],
|
||||
)
|
||||
|
||||
check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -12,6 +12,8 @@ from esphome.const import (
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32S3_SPI_PSRAM_PINS = {
|
||||
26: "SPICS1",
|
||||
27: "SPIHD",
|
||||
@@ -53,12 +55,7 @@ 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,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -73,6 +70,7 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Per the ESP32-S31 IDF DOCS and datasheet:
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
|
||||
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
|
||||
@@ -16,6 +18,8 @@ _ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
|
||||
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
|
||||
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
|
||||
|
||||
_ESP32S31_USB_JTAG_PINS = {33, 34}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -28,6 +32,7 @@ def esp32_s31_validate_gpio_pin(value: int) -> int:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -46,6 +51,8 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
@@ -74,7 +75,7 @@ ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void ESP8266GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
uint8_t arduino_mode = 0;
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
class ESP8266GPIOPin final : public InternalGPIOPin {
|
||||
class ESP8266GPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
@@ -18,14 +18,18 @@ class ESP8266GPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return pin_; }
|
||||
gpio::Flags get_flags() const override { return flags_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
bool is_inverted() const { return inverted_; }
|
||||
bool is_internal() override { return true; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
|
||||
@@ -635,7 +635,7 @@ error:
|
||||
|
||||
this->cleanup_connection_();
|
||||
|
||||
this->status_momentary_error("err", 5000);
|
||||
this->status_momentary_error(5000);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
|
||||
@@ -406,7 +406,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
|
||||
if (packet == nullptr) {
|
||||
this->send_packet_queue_.increment_dropped_count();
|
||||
ESP_LOGE(TAG, "Failed to allocate send packet from pool");
|
||||
this->status_momentary_warning("send-packet-pool-full");
|
||||
this->status_momentary_warning();
|
||||
return ESP_ERR_ESPNOW_NO_MEM;
|
||||
}
|
||||
// Load the packet data
|
||||
@@ -434,7 +434,7 @@ void ESPNowComponent::send_() {
|
||||
if (packet->callback_ != nullptr) {
|
||||
packet->callback_(err);
|
||||
}
|
||||
this->status_momentary_warning("send-failed");
|
||||
this->status_momentary_warning();
|
||||
this->send_packet_pool_.release(packet);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet
|
||||
return;
|
||||
@@ -447,7 +447,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
|
||||
}
|
||||
|
||||
if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
|
||||
this->status_momentary_warning("peer-add-failed");
|
||||
this->status_momentary_warning();
|
||||
return ESP_ERR_INVALID_MAC;
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
|
||||
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(peer, peer_buf);
|
||||
ESP_LOGE(TAG, "Failed to add peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-add-failed");
|
||||
this->status_momentary_warning();
|
||||
return err;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +492,7 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
|
||||
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(peer, peer_buf);
|
||||
ESP_LOGE(TAG, "Failed to delete peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-del-failed");
|
||||
this->status_momentary_warning();
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.const import CONF_ID, CONF_PIN, CONF_POWER_SUPPLY
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
@@ -17,6 +19,27 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def _require_hold(ps_config: ConfigType) -> ConfigType:
|
||||
if not ps_config[CONF_PIN].get(CONF_HOLD_STATE):
|
||||
raise cv.Invalid(
|
||||
f"The power supply pin must also set {CONF_HOLD_STATE} "
|
||||
f"when an output it powers sets it",
|
||||
[CONF_PIN],
|
||||
)
|
||||
return ps_config
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if not config[CONF_PIN].get(CONF_HOLD_STATE):
|
||||
return
|
||||
if (ps_id := config.get(CONF_POWER_SUPPLY)) is None:
|
||||
return
|
||||
fv.id_declaration_match_schema(_require_hold)(ps_id)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
|
||||
@@ -11,9 +11,12 @@ class GPIOBinaryOutput final : public output::BinaryOutput, public Component {
|
||||
void set_pin(GPIOPin *pin) { pin_ = pin; }
|
||||
|
||||
void setup() override {
|
||||
this->turn_off();
|
||||
const bool held = this->pin_->is_held();
|
||||
if (!held)
|
||||
this->turn_off();
|
||||
this->pin_->setup();
|
||||
this->turn_off();
|
||||
if (!held)
|
||||
this->turn_off();
|
||||
}
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -12,8 +12,9 @@ float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE;
|
||||
void GPIOSwitch::setup() {
|
||||
bool initial_state = this->get_initial_state_with_restore_mode().value_or(false);
|
||||
|
||||
// write state before setup
|
||||
this->control(initial_state);
|
||||
// write state before setup unless the pin kept it across the reset
|
||||
if (!this->pin_->is_held())
|
||||
this->control(initial_state);
|
||||
this->pin_->setup();
|
||||
// write after setup again for other IOs
|
||||
this->control(initial_state);
|
||||
|
||||
@@ -13,7 +13,6 @@ from esphome.const import (
|
||||
CONF_FONT,
|
||||
CONF_FOREGROUND_COLOR,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -29,9 +28,6 @@ MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArgument
|
||||
MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator(
|
||||
"const"
|
||||
)
|
||||
GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_(
|
||||
"GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@MrMDavidson"]
|
||||
DOMAIN = "graphical_display_menu"
|
||||
@@ -49,13 +45,7 @@ CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
|
||||
cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||
cv.Optional(CONF_ON_REDRAW): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
GraphicalDisplayMenuOnRedrawTrigger
|
||||
)
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_REDRAW): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -92,9 +82,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_background_color(background_color))
|
||||
|
||||
for conf in config.get(CONF_ON_REDRAW, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_redraw_callback", [(GraphicalDisplayMenuConstPtr, "it")], conf
|
||||
)
|
||||
|
||||
await display_menu_to_code(var, config)
|
||||
|
||||
@@ -241,6 +241,6 @@ void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, co
|
||||
"draw_item should be called.");
|
||||
}
|
||||
|
||||
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); }
|
||||
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(this); }
|
||||
|
||||
} // namespace esphome::graphical_display_menu
|
||||
|
||||
@@ -70,17 +70,7 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen
|
||||
Color foreground_color_{COLOR_ON};
|
||||
Color background_color_{COLOR_OFF};
|
||||
|
||||
CallbackManager<void()> on_redraw_callbacks_{};
|
||||
};
|
||||
|
||||
class GraphicalDisplayMenuOnRedrawTrigger final : public Trigger<const GraphicalDisplayMenu *> {
|
||||
public:
|
||||
explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) {
|
||||
parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
GraphicalDisplayMenu *parent_;
|
||||
LazyCallbackManager<void(const GraphicalDisplayMenu *)> on_redraw_callbacks_{};
|
||||
};
|
||||
|
||||
} // namespace graphical_display_menu
|
||||
|
||||
@@ -187,15 +187,18 @@ void HeatpumpIRClimate::transmit_state() {
|
||||
swing_h_cmd = HDIR_SWING;
|
||||
}
|
||||
|
||||
// MitsubishiMSCHeatpumpIR only knows FAN_1..FAN_3 and sends anything else as auto,
|
||||
// so low/medium/high have to start at FAN_1 there instead of FAN_2.
|
||||
const bool three_speed_fan = this->protocol_ == PROTOCOL_MITSUBISHI_MSC;
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_speed_cmd = FAN_2;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_1 : FAN_2;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_speed_cmd = FAN_3;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_2 : FAN_3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_speed_cmd = FAN_4;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_3 : FAN_4;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -20,7 +21,7 @@ ISRInternalGPIOPin HostGPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void HostGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type);
|
||||
}
|
||||
void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); }
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
|
||||
namespace esphome::host {
|
||||
|
||||
class HostGPIOPin final : public InternalGPIOPin {
|
||||
class HostGPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
@@ -17,14 +17,18 @@ class HostGPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return pin_; }
|
||||
gpio::Flags get_flags() const override { return flags_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
bool is_inverted() const { return inverted_; }
|
||||
bool is_internal() override { return true; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
|
||||
@@ -28,7 +28,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
|
||||
if (!status) {
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url);
|
||||
container->end();
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -143,13 +143,13 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url,
|
||||
HTTPClient::errorToString(container->status_code).c_str());
|
||||
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
container->end();
|
||||
return nullptr;
|
||||
}
|
||||
if (!is_success(container->status_code)) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
// Still return the container, so it can be used to get the status code and error message
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
if (!result) {
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url, (unsigned) result.error());
|
||||
container->end();
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
return nullptr;
|
||||
}
|
||||
App.feed_wdt();
|
||||
@@ -109,7 +109,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
container->status_code = response.status;
|
||||
if (!is_success(response.status)) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, response.status);
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
// Still return the container, so it can be used to get the status code and error message
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
} else if (strcmp(method, "PATCH") == 0) {
|
||||
method_idf = HTTP_METHOD_PATCH;
|
||||
} else {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -110,7 +110,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (client == nullptr) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
|
||||
esp_err_t err = esp_http_client_open(client, body_len);
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
@@ -154,7 +154,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
@@ -179,7 +179,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
err = esp_http_client_set_redirection(client);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -220,7 +220,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ void I2SAudioMicrophone::loop() {
|
||||
|
||||
if (!this->start_driver_()) {
|
||||
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("driver_fail", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_driver_(); // Stop/frees whatever possibly started
|
||||
break;
|
||||
}
|
||||
@@ -408,7 +408,7 @@ void I2SAudioMicrophone::loop() {
|
||||
|
||||
if (this->task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
|
||||
this->status_momentary_error("task_fail", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
|
||||
if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("driver-failure", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
|
||||
if (this->speaker_task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
|
||||
this->status_momentary_error("task-failure", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -2,13 +2,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, improv_base, output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_ON_START,
|
||||
CONF_ON_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, PLATFORM_ESP32
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -59,20 +53,35 @@ State = improv_ns.enum("State")
|
||||
|
||||
improv_ble_ns = cg.esphome_ns.namespace("improv_ble")
|
||||
ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component)
|
||||
ImprovBLEProvisionedTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEProvisionedTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEProvisioningTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEProvisioningTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStartTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStartTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStateTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStateTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStoppedTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStoppedTrigger", automation.Trigger.template()
|
||||
|
||||
_STATE_PARAMS = [(State, "state"), (Error, "error")]
|
||||
_NOT_FAILED = "!{parent}->is_failed()"
|
||||
|
||||
|
||||
def _state_automation(
|
||||
conf_key: str, args: list, states: str | None
|
||||
) -> automation.CallbackAutomation:
|
||||
# Every automation hangs off the state callback and is silent while the component has failed.
|
||||
return automation.CallbackAutomation(
|
||||
conf_key,
|
||||
"add_on_state_callback",
|
||||
args,
|
||||
params=_STATE_PARAMS,
|
||||
forward=[name for _, name in args],
|
||||
when=_NOT_FAILED if states is None else f"({states}) && {_NOT_FAILED}",
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
_state_automation(CONF_ON_PROVISIONED, [], "state == improv::STATE_PROVISIONED"),
|
||||
_state_automation(CONF_ON_PROVISIONING, [], "state == improv::STATE_PROVISIONING"),
|
||||
_state_automation(
|
||||
CONF_ON_START,
|
||||
[],
|
||||
"state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION",
|
||||
),
|
||||
_state_automation(CONF_ON_STATE, _STATE_PARAMS, None),
|
||||
_state_automation(CONF_ON_STOP, [], "state == improv::STATE_STOPPED"),
|
||||
)
|
||||
|
||||
|
||||
@@ -93,41 +102,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStateTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
.extend(improv_base.IMPROV_SCHEMA)
|
||||
@@ -164,28 +143,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR])
|
||||
cg.add(var.set_status_indicator(status_indicator))
|
||||
|
||||
use_state_callback = False
|
||||
for conf in config.get(CONF_ON_PROVISIONED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_PROVISIONING, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(State, "state"), (Error, "error")], conf
|
||||
)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_STOP, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
if use_state_callback:
|
||||
if any(config.get(entry.conf_key) for entry in _CALLBACK_AUTOMATIONS):
|
||||
cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK")
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#include "improv_ble_component.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <improv.h>
|
||||
|
||||
namespace esphome::improv_ble {
|
||||
|
||||
class ImprovBLEProvisionedTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEProvisioningTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStartTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
|
||||
!this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStateTrigger final : public Trigger<improv::State, improv::Error> {
|
||||
public:
|
||||
explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (!this->parent_->is_failed()) {
|
||||
this->trigger(state, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStoppedTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::improv_ble
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "gpio_arduino.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::libretiny {
|
||||
@@ -37,7 +38,7 @@ ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
PinStatus arduino_mode = (PinStatus) 255;
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
|
||||
namespace esphome::libretiny {
|
||||
|
||||
class ArduinoInternalGPIOPin final : public InternalGPIOPin {
|
||||
class ArduinoInternalGPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
@@ -16,14 +16,18 @@ class ArduinoInternalGPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return pin_; }
|
||||
gpio::Flags get_flags() const override { return flags_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
bool is_inverted() const { return inverted_; }
|
||||
bool is_internal() override { return true; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
|
||||
@@ -41,7 +41,7 @@ from esphome.core.entity_helpers import (
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -71,6 +71,7 @@ from .types import ( # noqa: F401
|
||||
ChannelColors,
|
||||
ColorMode,
|
||||
GammaTable,
|
||||
LightEffect,
|
||||
LightOutput,
|
||||
LightState,
|
||||
LightStateRTCState,
|
||||
@@ -519,6 +520,17 @@ def validate_color_temperature_channels(value):
|
||||
return value
|
||||
|
||||
|
||||
def _effects_table(effects: list[MockObj]) -> MockObj:
|
||||
"""Emit the flash table of a light's effect pointers; each must be a static address."""
|
||||
return cg.shared_progmem_array(
|
||||
"light_effects",
|
||||
LightEffect.operator("ptr"),
|
||||
cg.ArrayInitializer(*effects),
|
||||
share=False,
|
||||
constexpr=False,
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("light")
|
||||
async def setup_light_core_(light_var, config, output_var):
|
||||
# All 8 legacy restore_mode values, and the restore_state key, are just different
|
||||
@@ -586,7 +598,7 @@ async def setup_light_core_(light_var, config, output_var):
|
||||
EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
|
||||
)
|
||||
if effects:
|
||||
cg.add(light_var.add_effects(effects))
|
||||
cg.add(light_var.add_effects(_effects_table(effects), len(effects)))
|
||||
|
||||
for conf in config.get(CONF_ON_TURN_ON, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
|
||||
|
||||
@@ -147,7 +147,7 @@ void LightCall::perform() {
|
||||
// EFFECT
|
||||
StringRef effect_s;
|
||||
if (this->effect_ == 0u) {
|
||||
effect_s = StringRef::from_lit("None");
|
||||
effect_s = EFFECT_NONE_REF;
|
||||
} else {
|
||||
effect_s = this->parent_->effects_[this->effect_ - 1]->get_name();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
inline constexpr StringRef EFFECT_NONE_REF = StringRef::from_lit("None");
|
||||
|
||||
class LightState;
|
||||
|
||||
class LightEffect {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "light_effect.h"
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
/// Effect names as the native API lists them: "None" first, then each effect; empty without effects.
|
||||
/// Reads the light's effect table directly, so listing entities allocates nothing. Holds a reference:
|
||||
/// it only lives for one synchronous encode, and by value costs flash on every copy.
|
||||
class LightEffectNames {
|
||||
public:
|
||||
explicit LightEffectNames(const ConstVector<LightEffect *> &effects) : effects_(effects) {}
|
||||
|
||||
class Iterator {
|
||||
public:
|
||||
ESPHOME_ALWAYS_INLINE Iterator(const ConstVector<LightEffect *> &effects, size_t index)
|
||||
: effects_(effects), index_(index) {}
|
||||
ESPHOME_ALWAYS_INLINE StringRef operator*() const {
|
||||
return this->index_ == 0 ? EFFECT_NONE_REF : this->effects_[this->index_ - 1]->get_name();
|
||||
}
|
||||
ESPHOME_ALWAYS_INLINE Iterator &operator++() {
|
||||
++this->index_;
|
||||
return *this;
|
||||
}
|
||||
ESPHOME_ALWAYS_INLINE bool operator!=(const Iterator &other) const { return this->index_ != other.index_; }
|
||||
|
||||
protected:
|
||||
const ConstVector<LightEffect *> &effects_;
|
||||
size_t index_;
|
||||
};
|
||||
|
||||
ESPHOME_ALWAYS_INLINE Iterator begin() const { return {this->effects_, 0}; }
|
||||
ESPHOME_ALWAYS_INLINE Iterator end() const {
|
||||
return {this->effects_, this->effects_.empty() ? 0 : this->effects_.size() + 1};
|
||||
}
|
||||
ESPHOME_ALWAYS_INLINE bool empty() const { return this->effects_.empty(); }
|
||||
|
||||
protected:
|
||||
const ConstVector<LightEffect *> &effects_;
|
||||
};
|
||||
|
||||
} // namespace esphome::light
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "light_output.h"
|
||||
#include "transformers.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "light");
|
||||
@@ -196,8 +194,6 @@ void LightState::publish_state() {
|
||||
|
||||
LightOutput *LightState::get_output() const { return this->output_; }
|
||||
|
||||
static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None");
|
||||
|
||||
StringRef LightState::get_effect_name() {
|
||||
if (this->active_effect_index_ > 0) {
|
||||
return this->effects_[this->active_effect_index_ - 1]->get_name();
|
||||
@@ -218,11 +214,6 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen
|
||||
this->target_state_reached_listeners_->push_back(listener);
|
||||
}
|
||||
|
||||
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_ = effects;
|
||||
}
|
||||
|
||||
void LightState::current_values_as_brightness(float *brightness) {
|
||||
this->current_values.as_brightness(brightness);
|
||||
*brightness = this->gamma_correct_lut(*brightness);
|
||||
@@ -352,9 +343,6 @@ float LightState::gamma_uncorrect_lut(float value) const {
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
void LightState::start_effect_(uint32_t effect_index) {
|
||||
// An external add_effects() can exceed the codegen cap; ignore an index the uint16_t can't hold
|
||||
if (effect_index > std::numeric_limits<uint16_t>::max())
|
||||
return;
|
||||
this->stop_effect_();
|
||||
if (effect_index == 0)
|
||||
return;
|
||||
|
||||
@@ -217,10 +217,13 @@ class LightState : public EntityBase, public Component {
|
||||
bool supports_effects() const { return !this->effects_.empty(); }
|
||||
|
||||
/// Get all effects for this light state.
|
||||
const FixedVector<LightEffect *> &get_effects() const { return this->effects_; }
|
||||
const ConstVector<LightEffect *> &get_effects() const { return this->effects_; }
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::initializer_list<LightEffect *> &effects);
|
||||
/// Codegen only: point at the generated table of this light's effects, which must outlive it.
|
||||
/// Codegen caps the count at 65535, so an effect index always fits the uint16_t active index.
|
||||
void add_effects(LightEffect *const *effects, size_t count) {
|
||||
this->effects_ = ConstVector<LightEffect *>(effects, count);
|
||||
}
|
||||
|
||||
/// Get the total number of effects available for this light.
|
||||
size_t get_effect_count() const { return this->effects_.size(); }
|
||||
@@ -354,7 +357,7 @@ class LightState : public EntityBase, public Component {
|
||||
/// The currently active transformer for this light (transition/flash).
|
||||
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
||||
/// List of effects for this light.
|
||||
FixedVector<LightEffect *> effects_;
|
||||
ConstVector<LightEffect *> effects_;
|
||||
/// Object used to store the persisted values of the light.
|
||||
ESPPreferenceObject rtc_;
|
||||
|
||||
|
||||
@@ -471,14 +471,14 @@ void MicroWakeWord::loop() {
|
||||
// uses floating point operations.
|
||||
if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_,
|
||||
this->microphone_source_->get_audio_stream_info().get_sample_rate())) {
|
||||
this->status_momentary_error("frontend_alloc", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->inference_task_.create(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE,
|
||||
(void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_)) {
|
||||
FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state
|
||||
this->status_momentary_error("task_start", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -12,7 +12,6 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MICROPHONE,
|
||||
CONF_ON_DATA,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
@@ -30,17 +29,12 @@ microphone_ns = cg.esphome_ns.namespace("microphone")
|
||||
Microphone = microphone_ns.class_("Microphone")
|
||||
MicrophoneSource = microphone_ns.class_("MicrophoneSource")
|
||||
|
||||
DataTrigger = microphone_ns.class_(
|
||||
"DataTrigger",
|
||||
automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")),
|
||||
)
|
||||
|
||||
|
||||
async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None:
|
||||
for conf in config.get(CONF_ON_DATA, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_data_callback",
|
||||
[(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")],
|
||||
conf,
|
||||
)
|
||||
@@ -54,11 +48,7 @@ async def register_microphone(var: MockObj, config: ConfigType) -> None:
|
||||
|
||||
MICROPHONE_SCHEMA = cv.Schema.extend(audio.AUDIO_COMPONENT_SCHEMA).extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_DATA): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "microphone.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::microphone {
|
||||
|
||||
class DataTrigger final : public Trigger<const std::vector<uint8_t> &> {
|
||||
public:
|
||||
explicit DataTrigger(Microphone *mic) {
|
||||
mic->add_data_callback([this](const std::vector<uint8_t> &data) { this->trigger(data); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::microphone
|
||||
@@ -359,7 +359,7 @@ void MixerSpeaker::loop() {
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("failure", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() {
|
||||
this->button_->press();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", LOG_STR_ARG(this->log_name_()), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
|
||||
} else {
|
||||
delay(5);
|
||||
ESP_LOGV(TAG, "Subscribe failed for topic='%s'. Will retry", topic);
|
||||
this->status_momentary_warning("subscribe", 1000);
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
return ret != 0;
|
||||
}
|
||||
@@ -499,7 +499,7 @@ void MQTTClientComponent::unsubscribe(const std::string &topic) {
|
||||
} else {
|
||||
delay(5);
|
||||
ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str());
|
||||
this->status_momentary_warning("unsubscribe", 1000);
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
|
||||
auto it = subscriptions_.begin();
|
||||
@@ -553,7 +553,7 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t
|
||||
ESP_LOGVV(TAG, "Publish payload (len=%u): '%.*s'", payload_length, static_cast<int>(payload_length), payload);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", topic, payload_length);
|
||||
this->status_momentary_warning("publish", 1000);
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
}
|
||||
return ret != 0;
|
||||
|
||||
@@ -46,7 +46,7 @@ void MQTTFanComponent::setup() {
|
||||
case PARSE_NONE:
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown state payload %s", payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -71,7 +71,7 @@ void MQTTFanComponent::setup() {
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str());
|
||||
this->status_momentary_warning("direction", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -95,7 +95,7 @@ void MQTTFanComponent::setup() {
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
ESP_LOGW(TAG, "Unknown Oscillation Payload %s", payload.c_str());
|
||||
this->status_momentary_warning("oscillation", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -112,11 +112,11 @@ void MQTTFanComponent::setup() {
|
||||
this->state_->make_call().set_speed(speed_level).perform();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid speed level %d", speed_level);
|
||||
this->status_momentary_warning("speed", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid speed level %s (int expected)", payload.c_str());
|
||||
this->status_momentary_warning("speed", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ void MQTTLockComponent::setup() {
|
||||
this->lock_->open();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", LOG_STR_ARG(this->log_name_()), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
}
|
||||
});
|
||||
this->lock_->add_on_state_callback(
|
||||
|
||||
@@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() {
|
||||
case PARSE_NONE:
|
||||
default:
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", LOG_STR_ARG(this->log_name_()), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() {
|
||||
this->update_->perform();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", LOG_STR_ARG(this->log_name_()), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
this->status_momentary_warning(5000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
|
||||
from . import const, generate, schema, validate
|
||||
@@ -27,14 +27,16 @@ CONF_SYNC_MODE = "sync_mode"
|
||||
CONF_BEFORE_SEND = "before_send"
|
||||
CONF_BEFORE_PROCESS_RESPONSE = "before_process_response"
|
||||
|
||||
# Triggers
|
||||
BeforeSendTrigger = generate.opentherm_ns.class_(
|
||||
"BeforeSendTrigger",
|
||||
automation.Trigger.template(generate.OpenthermData.operator("ref")),
|
||||
)
|
||||
BeforeProcessResponseTrigger = generate.opentherm_ns.class_(
|
||||
"BeforeProcessResponseTrigger",
|
||||
automation.Trigger.template(generate.OpenthermData.operator("ref")),
|
||||
_DATA_ARGS = [(generate.OpenthermData.operator("ref"), "x")]
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_BEFORE_SEND, "add_on_before_send_callback", _DATA_ARGS
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_BEFORE_PROCESS_RESPONSE,
|
||||
"add_on_before_process_response_callback",
|
||||
_DATA_ARGS,
|
||||
),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -51,17 +53,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
|
||||
cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
|
||||
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
|
||||
cv.Optional(CONF_BEFORE_SEND): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_BEFORE_SEND): automation.validate_automation({}),
|
||||
cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BeforeProcessResponseTrigger
|
||||
),
|
||||
}
|
||||
{}
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -132,14 +126,4 @@ async def to_code(config: dict[str, Any]) -> None:
|
||||
generate.define_setting_readers(const.SETTING, settings)
|
||||
generate.add_messages(var, settings, schema.SETTINGS)
|
||||
|
||||
for conf in config.get(CONF_BEFORE_SEND, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "hub.h"
|
||||
#include "opentherm.h"
|
||||
|
||||
namespace esphome::opentherm {
|
||||
|
||||
class BeforeSendTrigger final : public Trigger<OpenthermData &> {
|
||||
public:
|
||||
BeforeSendTrigger(OpenthermHub *hub) {
|
||||
hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); });
|
||||
}
|
||||
};
|
||||
|
||||
class BeforeProcessResponseTrigger final : public Trigger<OpenthermData &> {
|
||||
public:
|
||||
BeforeProcessResponseTrigger(OpenthermHub *hub) {
|
||||
hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::opentherm
|
||||
@@ -9,7 +9,12 @@ static constexpr uint32_t POWER_OFF_TIMEOUT_ID = 0;
|
||||
|
||||
void PowerSupply::setup() {
|
||||
this->pin_->setup();
|
||||
this->pin_->digital_write(false);
|
||||
if (this->pin_->is_held()) {
|
||||
// Rail stayed on across the reset; drop it unless something asks for it
|
||||
this->schedule_off_if_idle_();
|
||||
} else {
|
||||
this->pin_->digital_write(false);
|
||||
}
|
||||
if (this->enable_on_boot_)
|
||||
this->request_high_power();
|
||||
}
|
||||
@@ -47,6 +52,10 @@ void PowerSupply::unrequest_high_power() {
|
||||
return;
|
||||
}
|
||||
this->active_requests_--;
|
||||
this->schedule_off_if_idle_();
|
||||
}
|
||||
|
||||
void PowerSupply::schedule_off_if_idle_() {
|
||||
if (this->active_requests_ == 0) {
|
||||
this->set_timeout(POWER_OFF_TIMEOUT_ID, this->keep_on_time_, [this]() {
|
||||
ESP_LOGV(TAG, "Disabling");
|
||||
@@ -54,7 +63,10 @@ void PowerSupply::unrequest_high_power() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PowerSupply::on_powerdown() {
|
||||
if (this->pin_->get_hold())
|
||||
return;
|
||||
this->active_requests_ = 0;
|
||||
this->pin_->digital_write(false);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class PowerSupply final : public Component {
|
||||
uint32_t keep_on_time_;
|
||||
int16_t active_requests_{0}; // use signed integer to make catching negative requests easier.
|
||||
bool enable_on_boot_{false};
|
||||
void schedule_off_if_idle_();
|
||||
};
|
||||
|
||||
class PowerSupplyRequester {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_RP2
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -40,7 +41,7 @@ ISRInternalGPIOPin RP2GPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void RP2GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void RP2GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
PinStatus arduino_mode = LOW;
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#ifdef USE_RP2
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
|
||||
namespace esphome::rp2 {
|
||||
|
||||
class RP2GPIOPin final : public InternalGPIOPin {
|
||||
class RP2GPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
@@ -18,14 +18,18 @@ class RP2GPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return pin_; }
|
||||
gpio::Flags get_flags() const override { return flags_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
bool is_inverted() const { return inverted_; }
|
||||
bool is_internal() override { return true; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
|
||||
@@ -171,7 +171,7 @@ bool SoundLevelComponent::start_() {
|
||||
(stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame;
|
||||
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
|
||||
if (temp_ring_buffer == nullptr) {
|
||||
this->status_momentary_error("ring_buffer", 15000);
|
||||
this->status_momentary_error(15000);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ bool SoundLevelComponent::start_() {
|
||||
this->audio_source_ = audio::RingBufferAudioSource::create(
|
||||
temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast<uint8_t>(bytes_per_frame));
|
||||
if (this->audio_source_ == nullptr) {
|
||||
this->status_momentary_error("audio_source", 15000);
|
||||
this->status_momentary_error(15000);
|
||||
return false;
|
||||
}
|
||||
this->ring_buffer_ = temp_ring_buffer;
|
||||
|
||||
@@ -218,7 +218,7 @@ static constexpr uint8_t PROGMEM
|
||||
100 }; // 100 ms delay
|
||||
|
||||
// clang-format on
|
||||
static const char *const TAG = "st7735";
|
||||
ESPHOME_LOG_TAG(TAG, "st7735");
|
||||
|
||||
ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr,
|
||||
bool invert_colors)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::st7789v {
|
||||
|
||||
static const char *const TAG = "st7789v";
|
||||
ESPHOME_LOG_TAG(TAG, "st7789v");
|
||||
#ifdef USE_ESP32
|
||||
static constexpr size_t TEMP_BUFFER_SIZE = 1024;
|
||||
#else
|
||||
|
||||
@@ -46,7 +46,6 @@ DOMAIN = "time"
|
||||
time_ns = cg.esphome_ns.namespace("time")
|
||||
RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent)
|
||||
CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component)
|
||||
SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component)
|
||||
|
||||
# C++ types for pre-parsed timezone struct generation
|
||||
DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True)
|
||||
@@ -357,11 +356,7 @@ TIME_SCHEMA = cv.Schema(
|
||||
},
|
||||
validate_cron_keys,
|
||||
),
|
||||
cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation({}),
|
||||
}
|
||||
).extend(
|
||||
# ``visibility=ADVANCED`` flags the inherited ``update_interval``
|
||||
@@ -424,8 +419,7 @@ async def setup_time_core_(time_var, config):
|
||||
_emit_parsed_timezone_fields(parsed)
|
||||
|
||||
on_time = config.get(CONF_ON_TIME, [])
|
||||
on_time_sync = config.get(CONF_ON_TIME_SYNC, [])
|
||||
if on_time or on_time_sync:
|
||||
if on_time:
|
||||
cg.add_define("USE_TIME_TRIGGERS")
|
||||
|
||||
for conf in on_time:
|
||||
@@ -447,11 +441,10 @@ async def setup_time_core_(time_var, config):
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in on_time_sync:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
|
||||
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_TIME_SYNC, []):
|
||||
await automation.build_callback_automation(
|
||||
time_var, "add_on_time_sync_callback", [], conf
|
||||
)
|
||||
|
||||
|
||||
async def register_time(time_var, config):
|
||||
@@ -478,8 +471,8 @@ automation.register_apply_condition(
|
||||
|
||||
|
||||
# posix_tz.cpp is fully #ifdef'd on USE_TIME_TIMEZONE, set only when a
|
||||
# timezone is configured or detected; automation.cpp holds the on_time and
|
||||
# on_time_sync triggers and is #ifdef'd on USE_TIME_TRIGGERS.
|
||||
# timezone is configured or detected; automation.cpp holds the on_time
|
||||
# trigger and is #ifdef'd on USE_TIME_TRIGGERS.
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{
|
||||
"posix_tz.cpp": "USE_TIME_TIMEZONE",
|
||||
|
||||
@@ -94,10 +94,6 @@ void CronTrigger::add_days_of_week(const std::vector<uint8_t> &days_of_week) {
|
||||
}
|
||||
float CronTrigger::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) {
|
||||
rtc->add_on_time_sync_callback([this]() { this->trigger(); });
|
||||
}
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
#endif // USE_TIME_TRIGGERS
|
||||
|
||||
@@ -43,14 +43,6 @@ class CronTrigger final : public Trigger<>, public Component {
|
||||
RealTimeClock *rtc_;
|
||||
optional<ESPTime> last_check_;
|
||||
};
|
||||
|
||||
class SyncTrigger final : public Trigger<>, public Component {
|
||||
public:
|
||||
explicit SyncTrigger(RealTimeClock *rtc);
|
||||
|
||||
protected:
|
||||
RealTimeClock *rtc_;
|
||||
};
|
||||
} // namespace esphome::time
|
||||
|
||||
#endif // USE_TIME_TRIGGERS
|
||||
|
||||
@@ -2,7 +2,8 @@ from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "tuya"
|
||||
@@ -17,6 +18,8 @@ tuya_ns = cg.esphome_ns.namespace("tuya")
|
||||
TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True)
|
||||
Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice)
|
||||
|
||||
TuyaDatapoint = tuya_ns.struct("TuyaDatapoint")
|
||||
|
||||
DPTYPE_ANY = "any"
|
||||
DPTYPE_RAW = "raw"
|
||||
DPTYPE_BOOL = "bool"
|
||||
@@ -26,60 +29,18 @@ DPTYPE_STRING = "string"
|
||||
DPTYPE_ENUM = "enum"
|
||||
DPTYPE_BITMASK = "bitmask"
|
||||
|
||||
# Automation argument type, plus the expected TuyaDatapointType and the field forwarded
|
||||
DATAPOINT_TYPES = {
|
||||
DPTYPE_ANY: tuya_ns.struct("TuyaDatapoint"),
|
||||
DPTYPE_RAW: cg.std_vector.template(cg.uint8),
|
||||
DPTYPE_BOOL: cg.bool_,
|
||||
DPTYPE_INT: cg.int_,
|
||||
DPTYPE_UINT: cg.uint32,
|
||||
DPTYPE_STRING: cg.std_string,
|
||||
DPTYPE_ENUM: cg.uint8,
|
||||
DPTYPE_BITMASK: cg.uint32,
|
||||
DPTYPE_ANY: (TuyaDatapoint, None, None),
|
||||
DPTYPE_RAW: (cg.std_vector.template(cg.uint8), TuyaDatapointType.RAW, "value_raw"),
|
||||
DPTYPE_BOOL: (cg.bool_, TuyaDatapointType.BOOLEAN, "value_bool"),
|
||||
DPTYPE_INT: (cg.int_, TuyaDatapointType.INTEGER, "value_int"),
|
||||
DPTYPE_UINT: (cg.uint32, TuyaDatapointType.INTEGER, "value_uint"),
|
||||
DPTYPE_STRING: (cg.std_string, TuyaDatapointType.STRING, "value_string"),
|
||||
DPTYPE_ENUM: (cg.uint8, TuyaDatapointType.ENUM, "value_enum"),
|
||||
DPTYPE_BITMASK: (cg.uint32, TuyaDatapointType.BITMASK, "value_bitmask"),
|
||||
}
|
||||
|
||||
DATAPOINT_TRIGGERS = {
|
||||
DPTYPE_ANY: tuya_ns.class_(
|
||||
"TuyaDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ANY]),
|
||||
),
|
||||
DPTYPE_RAW: tuya_ns.class_(
|
||||
"TuyaRawDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_RAW]),
|
||||
),
|
||||
DPTYPE_BOOL: tuya_ns.class_(
|
||||
"TuyaBoolDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BOOL]),
|
||||
),
|
||||
DPTYPE_INT: tuya_ns.class_(
|
||||
"TuyaIntDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_INT]),
|
||||
),
|
||||
DPTYPE_UINT: tuya_ns.class_(
|
||||
"TuyaUIntDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_UINT]),
|
||||
),
|
||||
DPTYPE_STRING: tuya_ns.class_(
|
||||
"TuyaStringDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_STRING]),
|
||||
),
|
||||
DPTYPE_ENUM: tuya_ns.class_(
|
||||
"TuyaEnumDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ENUM]),
|
||||
),
|
||||
DPTYPE_BITMASK: tuya_ns.class_(
|
||||
"TuyaBitmaskDatapointUpdateTrigger",
|
||||
automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BITMASK]),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def assign_declare_id(value):
|
||||
value = value.copy()
|
||||
value[CONF_TRIGGER_ID] = cv.declare_id(
|
||||
DATAPOINT_TRIGGERS[value[CONF_DATAPOINT_TYPE]]
|
||||
)(value[CONF_TRIGGER_ID].id)
|
||||
return value
|
||||
|
||||
|
||||
CONF_TUYA_ID = "tuya_id"
|
||||
CONFIG_SCHEMA = (
|
||||
@@ -93,15 +54,11 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DATAPOINT_TRIGGERS[DPTYPE_ANY]
|
||||
),
|
||||
cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_DATAPOINT_TYPE, default=DPTYPE_ANY): cv.one_of(
|
||||
*DATAPOINT_TRIGGERS, lower=True
|
||||
*DATAPOINT_TYPES, lower=True
|
||||
),
|
||||
},
|
||||
extra_validators=assign_declare_id,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -124,9 +81,11 @@ async def to_code(config):
|
||||
for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]:
|
||||
cg.add(var.add_ignore_mcu_update_on_datapoints(dp))
|
||||
for conf in config.get(CONF_ON_DATAPOINT_UPDATE, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, conf[CONF_SENSOR_DATAPOINT]
|
||||
)
|
||||
await automation.build_automation(
|
||||
trigger, [(DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]], "x")], conf
|
||||
type_, expected, field = DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]]
|
||||
forward = None
|
||||
if expected is not None:
|
||||
forward = [getattr(MockObj("x", ".").expect_type(expected), field)]
|
||||
callback = await automation.build_trigger_callback(
|
||||
[(type_, "x")], conf, params=[(TuyaDatapoint, "x")], forward=forward
|
||||
)
|
||||
cg.add(var.register_listener(conf[CONF_SENSOR_DATAPOINT], callback))
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "automation.h"
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "tuya.automation");
|
||||
|
||||
namespace esphome::tuya {
|
||||
|
||||
void check_expected_datapoint(const TuyaDatapoint &dp, TuyaDatapointType expected) {
|
||||
if (dp.type != expected) {
|
||||
ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", dp.id,
|
||||
static_cast<uint8_t>(expected), static_cast<uint8_t>(dp.type));
|
||||
}
|
||||
}
|
||||
|
||||
TuyaRawDatapointUpdateTrigger::TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::RAW);
|
||||
this->trigger(dp.value_raw);
|
||||
});
|
||||
}
|
||||
|
||||
TuyaBoolDatapointUpdateTrigger::TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::BOOLEAN);
|
||||
this->trigger(dp.value_bool);
|
||||
});
|
||||
}
|
||||
|
||||
TuyaIntDatapointUpdateTrigger::TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::INTEGER);
|
||||
this->trigger(dp.value_int);
|
||||
});
|
||||
}
|
||||
|
||||
TuyaUIntDatapointUpdateTrigger::TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::INTEGER);
|
||||
this->trigger(dp.value_uint);
|
||||
});
|
||||
}
|
||||
|
||||
TuyaStringDatapointUpdateTrigger::TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::STRING);
|
||||
this->trigger(dp.value_string);
|
||||
});
|
||||
}
|
||||
|
||||
TuyaEnumDatapointUpdateTrigger::TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::ENUM);
|
||||
this->trigger(dp.value_enum);
|
||||
});
|
||||
}
|
||||
|
||||
TuyaBitmaskDatapointUpdateTrigger::TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) {
|
||||
check_expected_datapoint(dp, TuyaDatapointType::BITMASK);
|
||||
this->trigger(dp.value_bitmask);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace esphome::tuya
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "tuya.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::tuya {
|
||||
|
||||
class TuyaDatapointUpdateTrigger final : public Trigger<TuyaDatapoint> {
|
||||
public:
|
||||
explicit TuyaDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) {
|
||||
parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { this->trigger(dp); });
|
||||
}
|
||||
};
|
||||
|
||||
class TuyaRawDatapointUpdateTrigger final : public Trigger<std::vector<uint8_t>> {
|
||||
public:
|
||||
explicit TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
class TuyaBoolDatapointUpdateTrigger final : public Trigger<bool> {
|
||||
public:
|
||||
explicit TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
class TuyaIntDatapointUpdateTrigger final : public Trigger<int> {
|
||||
public:
|
||||
explicit TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
class TuyaUIntDatapointUpdateTrigger final : public Trigger<uint32_t> {
|
||||
public:
|
||||
explicit TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
class TuyaStringDatapointUpdateTrigger final : public Trigger<std::string> {
|
||||
public:
|
||||
explicit TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
class TuyaEnumDatapointUpdateTrigger final : public Trigger<uint8_t> {
|
||||
public:
|
||||
explicit TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
class TuyaBitmaskDatapointUpdateTrigger final : public Trigger<uint32_t> {
|
||||
public:
|
||||
explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
|
||||
};
|
||||
|
||||
} // namespace esphome::tuya
|
||||
@@ -482,7 +482,7 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) {
|
||||
// Run through listeners
|
||||
for (auto &listener : this->listeners_) {
|
||||
if (listener.datapoint_id == datapoint.id)
|
||||
listener.on_datapoint(datapoint);
|
||||
listener.on_datapoint.call(datapoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,20 +796,27 @@ void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datap
|
||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_DELIVER, .payload = buffer});
|
||||
}
|
||||
|
||||
void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) {
|
||||
auto listener = TuyaDatapointListener{
|
||||
void Tuya::register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> func) {
|
||||
this->listeners_.push_back(TuyaDatapointListener{
|
||||
.datapoint_id = datapoint_id,
|
||||
.on_datapoint = func,
|
||||
};
|
||||
this->listeners_.push_back(listener);
|
||||
});
|
||||
|
||||
// Run through existing datapoints
|
||||
for (auto &datapoint : this->datapoints_) {
|
||||
if (datapoint.id == datapoint_id)
|
||||
func(datapoint);
|
||||
func.call(datapoint);
|
||||
}
|
||||
}
|
||||
|
||||
const TuyaDatapoint &TuyaDatapoint::expect_type(TuyaDatapointType expected) const {
|
||||
if (this->type != expected) {
|
||||
ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", this->id,
|
||||
static_cast<uint8_t>(expected), static_cast<uint8_t>(this->type));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
TuyaInitState Tuya::get_init_state() { return this->init_state_; }
|
||||
|
||||
} // namespace esphome::tuya
|
||||
|
||||
@@ -37,11 +37,14 @@ struct TuyaDatapoint {
|
||||
};
|
||||
std::string value_string;
|
||||
std::vector<uint8_t> value_raw;
|
||||
|
||||
/// Log a warning when this datapoint is not of type `expected`, and return it.
|
||||
const TuyaDatapoint &expect_type(TuyaDatapointType expected) const;
|
||||
};
|
||||
|
||||
struct TuyaDatapointListener {
|
||||
uint8_t datapoint_id;
|
||||
std::function<void(TuyaDatapoint)> on_datapoint;
|
||||
Callback<void(const TuyaDatapoint &)> on_datapoint;
|
||||
};
|
||||
|
||||
enum class TuyaCommandType : uint8_t {
|
||||
@@ -91,7 +94,9 @@ class Tuya final : public Component, public uart::UARTDevice {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
|
||||
template<typename F> void register_listener(uint8_t datapoint_id, F &&func) {
|
||||
this->register_listener_(datapoint_id, Callback<void(const TuyaDatapoint &)>::create(std::forward<F>(func)));
|
||||
}
|
||||
void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value);
|
||||
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value);
|
||||
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value);
|
||||
@@ -117,6 +122,7 @@ class Tuya final : public Component, public uart::UARTDevice {
|
||||
}
|
||||
|
||||
protected:
|
||||
void register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> func);
|
||||
void handle_char_(uint8_t c);
|
||||
void handle_datapoints_(const uint8_t *buffer, size_t len);
|
||||
optional<TuyaDatapoint> get_datapoint_(uint8_t datapoint_id);
|
||||
|
||||
@@ -533,6 +533,25 @@ def final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
def subtree_references_uart(
|
||||
node: object, uart_id: str, conf_key: str = CONF_UART_ID
|
||||
) -> bool:
|
||||
"""Return True if any dict in the subtree has a conf_key entry naming this bus.
|
||||
|
||||
For the final validation of a component that needs a UART for itself. Bare
|
||||
`id:` references (a uart.write action) and lambdas are not found.
|
||||
"""
|
||||
if isinstance(node, dict):
|
||||
return any(
|
||||
(key == conf_key and str(value) == uart_id)
|
||||
or subtree_references_uart(value, uart_id, conf_key)
|
||||
for key, value in node.items()
|
||||
)
|
||||
if isinstance(node, list):
|
||||
return any(subtree_references_uart(item, uart_id, conf_key) for item in node)
|
||||
return False
|
||||
|
||||
|
||||
async def register_uart_device(var, config):
|
||||
"""Register a UART device, setting up all the internal values.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user