Compare commits
127 Commits
beta
...
esp32_touc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55da83002b | ||
|
|
2dc85f5a42 | ||
|
|
82518b351d | ||
|
|
68f34a1683 | ||
|
|
bc6b72a422 | ||
|
|
99c86a3620 | ||
|
|
292f8ec880 | ||
|
|
599e28e1cb | ||
|
|
ee6b2ba6c6 | ||
|
|
05d2ec6e73 | ||
|
|
d1edb1e32a | ||
|
|
d1e6b8dd10 | ||
|
|
6bab7c80cb | ||
|
|
b32fc3bfdd | ||
|
|
1f14c316a3 | ||
|
|
1e24417db0 | ||
|
|
fb9387ecc5 | ||
|
|
6c5f4cdb70 | ||
|
|
aabacb7454 | ||
|
|
b5da84479e | ||
|
|
88d9361050 | ||
|
|
1d90388ffc | ||
|
|
b3c43ce31f | ||
|
|
6d9d22d422 | ||
|
|
86be1f56d0 | ||
|
|
a0c81ffd7a | ||
|
|
ec1dc42e58 | ||
|
|
866eaed73d | ||
|
|
a18374e1ad | ||
|
|
f7afcb3b24 | ||
|
|
3adcae783c | ||
|
|
73b40dd2e7 | ||
|
|
1e12614f9a | ||
|
|
aeaa7c699a | ||
|
|
f1c56b7254 | ||
|
|
e72e0d0646 | ||
|
|
5719d334aa | ||
|
|
bcb6b85333 | ||
|
|
5d765413ef | ||
|
|
efb2e5e7a8 | ||
|
|
5d5e346199 | ||
|
|
08a74890da | ||
|
|
0545b9c7f2 | ||
|
|
bbf7d32676 | ||
|
|
e83f4ae974 | ||
|
|
9b0d01e03f | ||
|
|
eae0d90a1e | ||
|
|
90c09a7650 | ||
|
|
aecf080211 | ||
|
|
8517420356 | ||
|
|
376be1f009 | ||
|
|
0021e76649 | ||
|
|
d440c4bc43 | ||
|
|
50840b2105 | ||
|
|
7502c6b6c0 | ||
|
|
919c32f0cc | ||
|
|
a28c951272 | ||
|
|
13d7c5a9a9 | ||
|
|
5f1383344d | ||
|
|
48f43d3eb1 | ||
|
|
4ac2141307 | ||
|
|
719d8cac97 | ||
|
|
99cbe53a8e | ||
|
|
a36af1bfac | ||
|
|
8b6aa319bf | ||
|
|
f945110369 | ||
|
|
a16d321e1a | ||
|
|
74e70278e2 | ||
|
|
1332e24a2c | ||
|
|
5ab78ec461 | ||
|
|
ce701d3c31 | ||
|
|
5fca1be44d | ||
|
|
0bd4c333bd | ||
|
|
c6ed880732 | ||
|
|
da0f3c6cce | ||
|
|
e5d12d346a | ||
|
|
478e2e726b | ||
|
|
dbdac3707b | ||
|
|
68d742465b | ||
|
|
f4d2dd1c2e | ||
|
|
86329a95a5 | ||
|
|
cdf8586fb0 | ||
|
|
f693926cea | ||
|
|
00a64d1e3a | ||
|
|
e901520d6a | ||
|
|
50cfa882ab | ||
|
|
aab80bb89d | ||
|
|
bd89a88e34 | ||
|
|
d322d83745 | ||
|
|
e732ac8a8c | ||
|
|
281405e0aa | ||
|
|
70dffff991 | ||
|
|
f64abe06fa | ||
|
|
97e1b60c61 | ||
|
|
ea6816ad33 | ||
|
|
463a581ab9 | ||
|
|
0ce523627f | ||
|
|
eae4bd222a | ||
|
|
2d0beef339 | ||
|
|
a7bb7fc14d | ||
|
|
e95ab19620 | ||
|
|
c047aa47eb | ||
|
|
61bca56316 | ||
|
|
11759a0ebe | ||
|
|
806b1f222e | ||
|
|
9a37323eb8 | ||
|
|
dac738a916 | ||
|
|
cdb5d61d76 | ||
|
|
99a54369bf | ||
|
|
f3572e7189 | ||
|
|
02a0584e43 | ||
|
|
261b561bb2 | ||
|
|
0228379a2e | ||
|
|
da79215bc3 | ||
|
|
a59e1c7011 | ||
|
|
f467c79a20 | ||
|
|
98a2f23024 | ||
|
|
c955897d1b | ||
|
|
cfdb0925ce | ||
|
|
83db3eddd9 | ||
|
|
cc2c5a544e | ||
|
|
8fba8c2800 | ||
|
|
51d1da8460 | ||
|
|
2f1257056d | ||
|
|
2f8f6967bf | ||
|
|
246527e618 | ||
|
|
3857cc9c83 |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.6.0b1
|
||||
PROJECT_NUMBER = 2025.7.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -227,7 +227,7 @@ bool APIServer::check_password(const std::string &password) const {
|
||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
|
||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
|
||||
@@ -54,7 +54,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_update(cover::Cover *obj) override;
|
||||
|
||||
@@ -46,12 +46,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
time_ = datetime.now()
|
||||
message: bytes = msg.message
|
||||
text = message.decode("utf8", "backslashreplace")
|
||||
if dashboard:
|
||||
text = text.replace("\033", "\\033")
|
||||
for parsed_msg in parse_log_message(
|
||||
text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]"
|
||||
):
|
||||
print(parsed_msg)
|
||||
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
|
||||
|
||||
stop = await async_run(cli, on_log, name=name)
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from logging import getLogger
|
||||
|
||||
from esphome import automation, core
|
||||
from esphome.automation import Condition, maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
from esphome.components.const import CONF_ON_STATE_CHANGE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DELAY,
|
||||
@@ -98,6 +101,7 @@ IS_PLATFORM_COMPONENT = True
|
||||
|
||||
CONF_TIME_OFF = "time_off"
|
||||
CONF_TIME_ON = "time_on"
|
||||
CONF_TRIGGER_ON_INITIAL_STATE = "trigger_on_initial_state"
|
||||
|
||||
DEFAULT_DELAY = "1s"
|
||||
DEFAULT_TIME_OFF = "100ms"
|
||||
@@ -127,9 +131,17 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
|
||||
StateTrigger = binary_sensor_ns.class_(
|
||||
"StateTrigger", automation.Trigger.template(bool)
|
||||
)
|
||||
StateChangeTrigger = binary_sensor_ns.class_(
|
||||
"StateChangeTrigger",
|
||||
automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)),
|
||||
)
|
||||
|
||||
BinarySensorPublishAction = binary_sensor_ns.class_(
|
||||
"BinarySensorPublishAction", automation.Action
|
||||
)
|
||||
BinarySensorInvalidateAction = binary_sensor_ns.class_(
|
||||
"BinarySensorInvalidateAction", automation.Action
|
||||
)
|
||||
|
||||
# Condition
|
||||
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
|
||||
@@ -144,6 +156,8 @@ AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Compon
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
FILTER_REGISTRY = Registry()
|
||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
||||
|
||||
@@ -386,6 +400,14 @@ def validate_click_timing(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_publish_initial_state(value):
|
||||
value = cv.boolean(value)
|
||||
_LOGGER.warning(
|
||||
"The 'publish_initial_state' option has been replaced by 'trigger_on_initial_state' and will be removed in a future release"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
_BINARY_SENSOR_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMPONENT_SCHEMA)
|
||||
@@ -395,7 +417,12 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTBinarySensorComponent
|
||||
),
|
||||
cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean,
|
||||
cv.Exclusive(
|
||||
CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
|
||||
): validate_publish_initial_state,
|
||||
cv.Exclusive(
|
||||
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
|
||||
@@ -454,6 +481,11 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -493,8 +525,10 @@ async def setup_binary_sensor_core_(var, config):
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE):
|
||||
cg.add(var.set_publish_initial_state(publish_initial_state))
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
cg.add(var.set_trigger_on_initial_state(trigger))
|
||||
if inverted := config.get(CONF_INVERTED):
|
||||
cg.add(var.set_inverted(inverted))
|
||||
if filters_config := config.get(CONF_FILTERS):
|
||||
@@ -542,6 +576,17 @@ async def setup_binary_sensor_core_(var, config):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(bool, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_STATE_CHANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.optional.template(bool), "x_previous"),
|
||||
(cg.optional.template(bool), "x"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
@@ -591,3 +636,18 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_BINARY_SENSOR")
|
||||
cg.add_global(binary_sensor_ns.using)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"binary_sensor.invalidate_state",
|
||||
BinarySensorInvalidateAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BinarySensor),
|
||||
},
|
||||
key=CONF_ID,
|
||||
),
|
||||
)
|
||||
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -96,7 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
: parent_(parent), timing_(std::move(timing)) {}
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->state;
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
|
||||
this->parent_->add_on_state_callback(f);
|
||||
}
|
||||
@@ -130,6 +130,14 @@ class StateTrigger : public Trigger<bool> {
|
||||
}
|
||||
};
|
||||
|
||||
class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
|
||||
public:
|
||||
explicit StateChangeTrigger(BinarySensor *parent) {
|
||||
parent->add_full_state_callback(
|
||||
[this](optional<bool> old_state, optional<bool> state) { this->trigger(old_state, state); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
|
||||
public:
|
||||
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
|
||||
@@ -154,5 +162,15 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
|
||||
|
||||
void play(Ts... x) override { this->sensor_->invalidate_state(); }
|
||||
|
||||
protected:
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -7,42 +7,25 @@ namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
void BinarySensor::add_on_state_callback(std::function<void(bool)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void BinarySensor::publish_state(bool state) {
|
||||
if (!this->publish_dedup_.next(state))
|
||||
return;
|
||||
void BinarySensor::publish_state(bool new_state) {
|
||||
if (this->filter_list_ == nullptr) {
|
||||
this->send_state_internal(state, false);
|
||||
this->send_state_internal(new_state);
|
||||
} else {
|
||||
this->filter_list_->input(state, false);
|
||||
this->filter_list_->input(new_state);
|
||||
}
|
||||
}
|
||||
void BinarySensor::publish_initial_state(bool state) {
|
||||
if (!this->publish_dedup_.next(state))
|
||||
return;
|
||||
if (this->filter_list_ == nullptr) {
|
||||
this->send_state_internal(state, true);
|
||||
} else {
|
||||
this->filter_list_->input(state, true);
|
||||
void BinarySensor::publish_initial_state(bool new_state) {
|
||||
this->invalidate_state();
|
||||
this->publish_state(new_state);
|
||||
}
|
||||
void BinarySensor::send_state_internal(bool new_state) {
|
||||
// copy the new state to the visible property for backwards compatibility, before any callbacks
|
||||
this->state = new_state;
|
||||
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
|
||||
if (this->set_state_(new_state)) {
|
||||
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
|
||||
}
|
||||
}
|
||||
void BinarySensor::send_state_internal(bool state, bool is_initial) {
|
||||
if (is_initial) {
|
||||
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state));
|
||||
}
|
||||
this->has_state_ = true;
|
||||
this->state = state;
|
||||
if (!is_initial || this->publish_initial_state_) {
|
||||
this->state_callback_.call(state);
|
||||
}
|
||||
}
|
||||
|
||||
BinarySensor::BinarySensor() : state(false) {}
|
||||
|
||||
void BinarySensor::add_filter(Filter *filter) {
|
||||
filter->parent_ = this;
|
||||
@@ -60,7 +43,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
this->add_filter(filter);
|
||||
}
|
||||
}
|
||||
bool BinarySensor::has_state() const { return this->has_state_; }
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/filter.h"
|
||||
@@ -34,52 +33,39 @@ namespace binary_sensor {
|
||||
* The sub classes should notify the front-end of new states via the publish_state() method which
|
||||
* handles inverted inputs for you.
|
||||
*/
|
||||
class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
|
||||
public:
|
||||
explicit BinarySensor();
|
||||
|
||||
/** Add a callback to be notified of state changes.
|
||||
*
|
||||
* @param callback The void(bool) callback.
|
||||
*/
|
||||
void add_on_state_callback(std::function<void(bool)> &&callback);
|
||||
explicit BinarySensor(){};
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
* @param state The new state.
|
||||
* @param new_state The new state.
|
||||
*/
|
||||
void publish_state(bool state);
|
||||
void publish_state(bool new_state);
|
||||
|
||||
/** Publish the initial state, this will not make the callback manager send callbacks
|
||||
* and is meant only for the initial state on boot.
|
||||
*
|
||||
* @param state The new state.
|
||||
* @param new_state The new state.
|
||||
*/
|
||||
void publish_initial_state(bool state);
|
||||
|
||||
/// The current reported state of the binary sensor.
|
||||
bool state{false};
|
||||
void publish_initial_state(bool new_state);
|
||||
|
||||
void add_filter(Filter *filter);
|
||||
void add_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; }
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
void send_state_internal(bool state, bool is_initial);
|
||||
void send_state_internal(bool new_state);
|
||||
|
||||
/// Return whether this binary sensor has outputted a state.
|
||||
virtual bool has_state() const;
|
||||
|
||||
virtual bool is_status_binary_sensor() const;
|
||||
|
||||
// For backward compatibility, provide an accessible property
|
||||
|
||||
bool state{};
|
||||
|
||||
protected:
|
||||
CallbackManager<void(bool)> state_callback_{};
|
||||
Filter *filter_list_{nullptr};
|
||||
bool has_state_{false};
|
||||
bool publish_initial_state_{false};
|
||||
Deduplicator<bool> publish_dedup_;
|
||||
};
|
||||
|
||||
class BinarySensorInitiallyOff : public BinarySensor {
|
||||
|
||||
@@ -9,37 +9,36 @@ namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
void Filter::output(bool value, bool is_initial) {
|
||||
void Filter::output(bool value) {
|
||||
if (this->next_ == nullptr) {
|
||||
this->parent_->send_state_internal(value);
|
||||
} else {
|
||||
this->next_->input(value);
|
||||
}
|
||||
}
|
||||
void Filter::input(bool value) {
|
||||
if (!this->dedup_.next(value))
|
||||
return;
|
||||
|
||||
if (this->next_ == nullptr) {
|
||||
this->parent_->send_state_internal(value, is_initial);
|
||||
} else {
|
||||
this->next_->input(value, is_initial);
|
||||
}
|
||||
}
|
||||
void Filter::input(bool value, bool is_initial) {
|
||||
auto b = this->new_value(value, is_initial);
|
||||
auto b = this->new_value(value);
|
||||
if (b.has_value()) {
|
||||
this->output(*b, is_initial);
|
||||
this->output(*b);
|
||||
}
|
||||
}
|
||||
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
|
||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
|
||||
} else {
|
||||
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
|
||||
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
|
||||
optional<bool> DelayedOnFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
|
||||
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout("ON");
|
||||
@@ -49,9 +48,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
|
||||
|
||||
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
|
||||
optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
if (!value) {
|
||||
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
|
||||
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout("OFF");
|
||||
@@ -61,11 +60,11 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
|
||||
|
||||
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
|
||||
|
||||
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
// Ignore if already running
|
||||
if (this->active_timing_ != 0)
|
||||
@@ -101,7 +100,7 @@ void AutorepeatFilter::next_timing_() {
|
||||
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val, false); // This is at least the second one so not initial
|
||||
this->output(val); // This is at least the second one so not initial
|
||||
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
|
||||
}
|
||||
|
||||
@@ -109,18 +108,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
|
||||
|
||||
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
|
||||
|
||||
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
|
||||
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
|
||||
|
||||
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
|
||||
optional<bool> SettleFilter::new_value(bool value) {
|
||||
if (!this->steady_) {
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
|
||||
this->steady_ = true;
|
||||
this->output(value, is_initial);
|
||||
this->output(value);
|
||||
});
|
||||
return {};
|
||||
} else {
|
||||
this->steady_ = false;
|
||||
this->output(value, is_initial);
|
||||
this->output(value);
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ class BinarySensor;
|
||||
|
||||
class Filter {
|
||||
public:
|
||||
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
|
||||
virtual optional<bool> new_value(bool value) = 0;
|
||||
|
||||
void input(bool value, bool is_initial);
|
||||
void input(bool value);
|
||||
|
||||
void output(bool value, bool is_initial);
|
||||
void output(bool value);
|
||||
|
||||
protected:
|
||||
friend BinarySensor;
|
||||
@@ -30,7 +30,7 @@ class Filter {
|
||||
|
||||
class DelayedOnOffFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
|
||||
|
||||
class DelayedOnFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
|
||||
|
||||
class DelayedOffFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
|
||||
|
||||
class InvertFilter : public Filter {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
};
|
||||
|
||||
struct AutorepeatFilterTiming {
|
||||
@@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
|
||||
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
|
||||
public:
|
||||
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
|
||||
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
protected:
|
||||
std::function<optional<bool>(bool)> f_;
|
||||
@@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
|
||||
|
||||
class SettleFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
touch_pad_init();
|
||||
// set up and enable/start filtering based on ESP32 variant
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
if (this->filter_configured_()) {
|
||||
touch_filter_config_t filter_info = {
|
||||
.mode = this->filter_mode_,
|
||||
.debounce_cnt = this->debounce_count_,
|
||||
.noise_thr = this->noise_threshold_,
|
||||
.jitter_step = this->jitter_step_,
|
||||
.smh_lvl = this->smooth_level_,
|
||||
};
|
||||
touch_pad_filter_set_config(&filter_info);
|
||||
touch_pad_filter_enable();
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
touch_pad_denoise_t denoise = {
|
||||
.grade = this->grade_,
|
||||
.cap_level = this->cap_level_,
|
||||
};
|
||||
touch_pad_denoise_set_config(&denoise);
|
||||
touch_pad_denoise_enable();
|
||||
}
|
||||
|
||||
if (this->waterproof_configured_()) {
|
||||
touch_pad_waterproof_t waterproof = {
|
||||
.guard_ring_pad = this->waterproof_guard_ring_pad_,
|
||||
.shield_driver = this->waterproof_shield_driver_,
|
||||
};
|
||||
touch_pad_waterproof_set_config(&waterproof);
|
||||
touch_pad_waterproof_enable();
|
||||
}
|
||||
#else
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_start(this->iir_filter_);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5 && defined(USE_ESP32_VARIANT_ESP32)
|
||||
touch_pad_set_measurement_clock_cycles(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
#else
|
||||
touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
|
||||
#endif
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
touch_pad_config(child->get_touch_pad());
|
||||
#else
|
||||
// Disable interrupt threshold
|
||||
touch_pad_config(child->get_touch_pad(), 0);
|
||||
#endif
|
||||
}
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
touch_pad_fsm_start();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Meas cycle: %.2fms\n"
|
||||
" Sleep cycle: %.2fms",
|
||||
this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f));
|
||||
|
||||
const char *lv_s;
|
||||
switch (this->low_voltage_reference_) {
|
||||
case TOUCH_LVOLT_0V5:
|
||||
lv_s = "0.5V";
|
||||
break;
|
||||
case TOUCH_LVOLT_0V6:
|
||||
lv_s = "0.6V";
|
||||
break;
|
||||
case TOUCH_LVOLT_0V7:
|
||||
lv_s = "0.7V";
|
||||
break;
|
||||
case TOUCH_LVOLT_0V8:
|
||||
lv_s = "0.8V";
|
||||
break;
|
||||
default:
|
||||
lv_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Low Voltage Reference: %s", lv_s);
|
||||
|
||||
const char *hv_s;
|
||||
switch (this->high_voltage_reference_) {
|
||||
case TOUCH_HVOLT_2V4:
|
||||
hv_s = "2.4V";
|
||||
break;
|
||||
case TOUCH_HVOLT_2V5:
|
||||
hv_s = "2.5V";
|
||||
break;
|
||||
case TOUCH_HVOLT_2V6:
|
||||
hv_s = "2.6V";
|
||||
break;
|
||||
case TOUCH_HVOLT_2V7:
|
||||
hv_s = "2.7V";
|
||||
break;
|
||||
default:
|
||||
hv_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " High Voltage Reference: %s", hv_s);
|
||||
|
||||
const char *atten_s;
|
||||
switch (this->voltage_attenuation_) {
|
||||
case TOUCH_HVOLT_ATTEN_1V5:
|
||||
atten_s = "1.5V";
|
||||
break;
|
||||
case TOUCH_HVOLT_ATTEN_1V:
|
||||
atten_s = "1V";
|
||||
break;
|
||||
case TOUCH_HVOLT_ATTEN_0V5:
|
||||
atten_s = "0.5V";
|
||||
break;
|
||||
case TOUCH_HVOLT_ATTEN_0V:
|
||||
atten_s = "0V";
|
||||
break;
|
||||
default:
|
||||
atten_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Voltage Attenuation: %s", atten_s);
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
if (this->filter_configured_()) {
|
||||
const char *filter_mode_s;
|
||||
switch (this->filter_mode_) {
|
||||
case TOUCH_PAD_FILTER_IIR_4:
|
||||
filter_mode_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_8:
|
||||
filter_mode_s = "IIR_8";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_16:
|
||||
filter_mode_s = "IIR_16";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_32:
|
||||
filter_mode_s = "IIR_32";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_64:
|
||||
filter_mode_s = "IIR_64";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_128:
|
||||
filter_mode_s = "IIR_128";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_256:
|
||||
filter_mode_s = "IIR_256";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_JITTER:
|
||||
filter_mode_s = "JITTER";
|
||||
break;
|
||||
default:
|
||||
filter_mode_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Filter mode: %s\n"
|
||||
" Debounce count: %" PRIu32 "\n"
|
||||
" Noise threshold coefficient: %" PRIu32 "\n"
|
||||
" Jitter filter step size: %" PRIu32,
|
||||
filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_);
|
||||
const char *smooth_level_s;
|
||||
switch (this->smooth_level_) {
|
||||
case TOUCH_PAD_SMOOTH_OFF:
|
||||
smooth_level_s = "OFF";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_2:
|
||||
smooth_level_s = "IIR_2";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_4:
|
||||
smooth_level_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_8:
|
||||
smooth_level_s = "IIR_8";
|
||||
break;
|
||||
default:
|
||||
smooth_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s);
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
const char *grade_s;
|
||||
switch (this->grade_) {
|
||||
case TOUCH_PAD_DENOISE_BIT12:
|
||||
grade_s = "BIT12";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT10:
|
||||
grade_s = "BIT10";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT8:
|
||||
grade_s = "BIT8";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT4:
|
||||
grade_s = "BIT4";
|
||||
break;
|
||||
default:
|
||||
grade_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s);
|
||||
|
||||
const char *cap_level_s;
|
||||
switch (this->cap_level_) {
|
||||
case TOUCH_PAD_DENOISE_CAP_L0:
|
||||
cap_level_s = "L0";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L1:
|
||||
cap_level_s = "L1";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L2:
|
||||
cap_level_s = "L2";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L3:
|
||||
cap_level_s = "L3";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L4:
|
||||
cap_level_s = "L4";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L5:
|
||||
cap_level_s = "L5";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L6:
|
||||
cap_level_s = "L6";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L7:
|
||||
cap_level_s = "L7";
|
||||
break;
|
||||
default:
|
||||
cap_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s);
|
||||
}
|
||||
#else
|
||||
if (this->iir_filter_enabled_()) {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter DISABLED");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
LOG_BINARY_SENSOR(" ", "Touch Pad", child);
|
||||
ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad());
|
||||
ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) {
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
uint32_t value = 0;
|
||||
if (this->filter_configured_()) {
|
||||
touch_pad_filter_read_smooth(tp, &value);
|
||||
} else {
|
||||
touch_pad_read_raw_data(tp, &value);
|
||||
}
|
||||
#else
|
||||
uint16_t value = 0;
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_read_filtered(tp, &value);
|
||||
} else {
|
||||
touch_pad_read(tp, &value);
|
||||
}
|
||||
#endif
|
||||
return value;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
|
||||
for (auto *child : this->children_) {
|
||||
child->value_ = this->component_touch_pad_read(child->get_touch_pad());
|
||||
#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3))
|
||||
child->publish_state(child->value_ < child->get_threshold());
|
||||
#else
|
||||
child->publish_state(child->value_ > child->get_threshold());
|
||||
#endif
|
||||
|
||||
if (should_print) {
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(),
|
||||
(uint32_t) child->get_touch_pad(), child->value_);
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
if (should_print) {
|
||||
// Avoid spamming logs
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
bool is_wakeup_source = false;
|
||||
|
||||
#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3))
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_stop();
|
||||
touch_pad_filter_delete();
|
||||
}
|
||||
#endif
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
if (!is_wakeup_source) {
|
||||
is_wakeup_source = true;
|
||||
// Touch sensor FSM mode must be 'TOUCH_FSM_MODE_TIMER' to use it to wake-up.
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
}
|
||||
|
||||
#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3))
|
||||
// No filter available when using as wake-up source.
|
||||
touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_wakeup_source) {
|
||||
touch_pad_deinit();
|
||||
}
|
||||
}
|
||||
|
||||
ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold)
|
||||
: touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
@@ -9,10 +9,19 @@
|
||||
#include <vector>
|
||||
|
||||
#include <driver/touch_sensor.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
// IMPORTANT: Touch detection logic differs between ESP32 variants:
|
||||
// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease)
|
||||
// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase)
|
||||
// This inversion is due to different hardware implementations between chip generations.
|
||||
|
||||
static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250;
|
||||
|
||||
class ESP32TouchBinarySensor;
|
||||
|
||||
class ESP32TouchComponent : public Component {
|
||||
@@ -31,6 +40,14 @@ class ESP32TouchComponent : public Component {
|
||||
void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) {
|
||||
this->voltage_attenuation_ = voltage_attenuation;
|
||||
}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void on_shutdown() override;
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; }
|
||||
void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; }
|
||||
@@ -47,17 +64,87 @@ class ESP32TouchComponent : public Component {
|
||||
void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
#endif
|
||||
|
||||
uint32_t component_touch_pad_read(touch_pad_t tp);
|
||||
protected:
|
||||
// Common helper methods
|
||||
void dump_config_base_();
|
||||
void dump_config_sensors_();
|
||||
bool create_touch_queue_();
|
||||
void cleanup_touch_queue_();
|
||||
void configure_wakeup_pads_();
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
// Common members
|
||||
std::vector<ESP32TouchBinarySensor *> children_;
|
||||
bool setup_mode_{false};
|
||||
uint32_t setup_mode_last_log_print_{0};
|
||||
|
||||
void on_shutdown() override;
|
||||
// Common configuration parameters
|
||||
uint16_t sleep_cycle_{4095};
|
||||
uint16_t meas_cycle_{65535};
|
||||
touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5};
|
||||
touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7};
|
||||
touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V};
|
||||
|
||||
// ==================== PLATFORM SPECIFIC ====================
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// ESP32 v1 specific
|
||||
static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100;
|
||||
|
||||
static void touch_isr_handler(void *arg);
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
|
||||
private:
|
||||
// Touch event structure for ESP32 v1
|
||||
// Contains touch pad info, value, and touch state for queue communication
|
||||
struct TouchPadEventV1 {
|
||||
touch_pad_t pad;
|
||||
uint32_t value;
|
||||
bool is_touched;
|
||||
};
|
||||
|
||||
protected:
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// Design note: last_touch_time_ does not require synchronization primitives because:
|
||||
// 1. ESP32 guarantees atomic 32-bit aligned reads/writes
|
||||
// 2. ISR only writes timestamps, main loop only reads (except sentinel value 1)
|
||||
// 3. Timing tolerance allows for occasional stale reads (50ms check interval)
|
||||
// 4. Queue operations provide implicit memory barriers
|
||||
// Using atomic/critical sections would add overhead without meaningful benefit
|
||||
uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0};
|
||||
uint32_t release_timeout_ms_{1500};
|
||||
uint32_t release_check_interval_ms_{50};
|
||||
uint32_t iir_filter_{0};
|
||||
|
||||
bool iir_filter_enabled_() const { return this->iir_filter_ > 0; }
|
||||
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// ESP32-S2/S3 v2 specific
|
||||
static void touch_isr_handler(void *arg);
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
|
||||
private:
|
||||
// Touch event structure for ESP32 v2 (S2/S3)
|
||||
// Contains touch pad and interrupt mask for queue communication
|
||||
struct TouchPadEventV2 {
|
||||
touch_pad_t pad;
|
||||
uint32_t intr_mask;
|
||||
};
|
||||
|
||||
protected:
|
||||
// Filter configuration
|
||||
touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX};
|
||||
uint32_t debounce_count_{0};
|
||||
uint32_t noise_threshold_{0};
|
||||
uint32_t jitter_step_{0};
|
||||
touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX};
|
||||
|
||||
// Denoise configuration
|
||||
touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX};
|
||||
touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX};
|
||||
|
||||
// Waterproof configuration
|
||||
touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX};
|
||||
touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX};
|
||||
|
||||
bool filter_configured_() const {
|
||||
return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX);
|
||||
}
|
||||
@@ -68,43 +155,78 @@ class ESP32TouchComponent : public Component {
|
||||
return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) &&
|
||||
(this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX);
|
||||
}
|
||||
#else
|
||||
bool iir_filter_enabled_() const { return this->iir_filter_ > 0; }
|
||||
|
||||
// Helper method to read touch values - non-blocking operation
|
||||
// Returns the current touch pad value using either filtered or raw reading
|
||||
// based on the filter configuration
|
||||
uint32_t read_touch_value(touch_pad_t pad) const;
|
||||
|
||||
// Helper to update touch state with a known state
|
||||
void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched);
|
||||
|
||||
// Helper to read touch value and update state for a given child
|
||||
void check_and_update_touch_state_(ESP32TouchBinarySensor *child);
|
||||
#endif
|
||||
|
||||
std::vector<ESP32TouchBinarySensor *> children_;
|
||||
bool setup_mode_{false};
|
||||
uint32_t setup_mode_last_log_print_{0};
|
||||
// common parameters
|
||||
uint16_t sleep_cycle_{4095};
|
||||
uint16_t meas_cycle_{65535};
|
||||
touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5};
|
||||
touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7};
|
||||
touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V};
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX};
|
||||
uint32_t debounce_count_{0};
|
||||
uint32_t noise_threshold_{0};
|
||||
uint32_t jitter_step_{0};
|
||||
touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX};
|
||||
touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX};
|
||||
touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX};
|
||||
touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX};
|
||||
touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX};
|
||||
#else
|
||||
uint32_t iir_filter_{0};
|
||||
#endif
|
||||
// Helper functions for dump_config - common to both implementations
|
||||
static const char *get_low_voltage_reference_str(touch_low_volt_t ref) {
|
||||
switch (ref) {
|
||||
case TOUCH_LVOLT_0V5:
|
||||
return "0.5V";
|
||||
case TOUCH_LVOLT_0V6:
|
||||
return "0.6V";
|
||||
case TOUCH_LVOLT_0V7:
|
||||
return "0.7V";
|
||||
case TOUCH_LVOLT_0V8:
|
||||
return "0.8V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_high_voltage_reference_str(touch_high_volt_t ref) {
|
||||
switch (ref) {
|
||||
case TOUCH_HVOLT_2V4:
|
||||
return "2.4V";
|
||||
case TOUCH_HVOLT_2V5:
|
||||
return "2.5V";
|
||||
case TOUCH_HVOLT_2V6:
|
||||
return "2.6V";
|
||||
case TOUCH_HVOLT_2V7:
|
||||
return "2.7V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) {
|
||||
switch (atten) {
|
||||
case TOUCH_HVOLT_ATTEN_1V5:
|
||||
return "1.5V";
|
||||
case TOUCH_HVOLT_ATTEN_1V:
|
||||
return "1V";
|
||||
case TOUCH_HVOLT_ATTEN_0V5:
|
||||
return "0.5V";
|
||||
case TOUCH_HVOLT_ATTEN_0V:
|
||||
return "0V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Simple helper class to expose a touch pad value as a binary sensor.
|
||||
class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
public:
|
||||
ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold);
|
||||
ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold)
|
||||
: touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {}
|
||||
|
||||
touch_pad_t get_touch_pad() const { return this->touch_pad_; }
|
||||
uint32_t get_threshold() const { return this->threshold_; }
|
||||
void set_threshold(uint32_t threshold) { this->threshold_ = threshold; }
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
uint32_t get_value() const { return this->value_; }
|
||||
#endif
|
||||
uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; }
|
||||
|
||||
protected:
|
||||
@@ -112,7 +234,10 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
|
||||
touch_pad_t touch_pad_{TOUCH_PAD_MAX};
|
||||
uint32_t threshold_{0};
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
uint32_t value_{0};
|
||||
#endif
|
||||
bool last_state_{false};
|
||||
const uint32_t wakeup_threshold_{0};
|
||||
};
|
||||
|
||||
|
||||
91
esphome/components/esp32_touch/esp32_touch_common.cpp
Normal file
91
esphome/components/esp32_touch/esp32_touch_common.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
void ESP32TouchComponent::dump_config_base_() {
|
||||
const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_);
|
||||
const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_);
|
||||
const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Meas cycle: %.2fms\n"
|
||||
" Sleep cycle: %.2fms\n"
|
||||
" Low Voltage Reference: %s\n"
|
||||
" High Voltage Reference: %s\n"
|
||||
" Voltage Attenuation: %s",
|
||||
this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s,
|
||||
atten_s);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config_sensors_() {
|
||||
for (auto *child : this->children_) {
|
||||
LOG_BINARY_SENSOR(" ", "Touch Pad", child);
|
||||
ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad());
|
||||
ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32TouchComponent::create_touch_queue_() {
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
// fires multiple times before main loop processes.
|
||||
size_t queue_size = this->children_.size() * 4;
|
||||
if (queue_size < 8)
|
||||
queue_size = 8;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1));
|
||||
#else
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2));
|
||||
#endif
|
||||
|
||||
if (this->touch_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::cleanup_touch_queue_() {
|
||||
if (this->touch_queue_) {
|
||||
vQueueDelete(this->touch_queue_);
|
||||
this->touch_queue_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::configure_wakeup_pads_() {
|
||||
bool is_wakeup_source = false;
|
||||
|
||||
// Check if any pad is configured for wakeup
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
is_wakeup_source = true;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// ESP32 v1: No filter available when using as wake-up source.
|
||||
touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#else
|
||||
// ESP32-S2/S3 v2: Set threshold for wakeup
|
||||
touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_wakeup_source) {
|
||||
// If no pad is configured for wakeup, deinitialize touch pad
|
||||
touch_pad_deinit();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
248
esphome/components/esp32_touch/esp32_touch_v1.cpp
Normal file
248
esphome/components/esp32_touch/esp32_touch_v1.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
// Include HAL for ISR-safe touch reading
|
||||
#include "hal/touch_sensor_ll.h"
|
||||
// Include for RTC clock frequency
|
||||
#include "soc/rtc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
// fires multiple times before main loop processes. This is important because
|
||||
// ESP32 v1 scans all pads on each interrupt, potentially sending multiple events.
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_pad_init();
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
|
||||
// Set up IIR filter if enabled
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_start(this->iir_filter_);
|
||||
}
|
||||
|
||||
// Configure measurement parameters
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
touch_pad_set_measurement_clock_cycles(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
#else
|
||||
touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
|
||||
#endif
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
|
||||
// Configure each touch pad
|
||||
for (auto *child : this->children_) {
|
||||
touch_pad_config(child->get_touch_pad(), child->get_threshold());
|
||||
}
|
||||
|
||||
// Register ISR handler
|
||||
esp_err_t err = touch_pad_isr_register(touch_isr_handler, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate release timeout based on sleep cycle
|
||||
// Design note: ESP32 v1 hardware limitation - interrupts only fire on touch (not release)
|
||||
// We must use timeout-based detection for release events
|
||||
// Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum
|
||||
// The division by 2 accounts for the fact that sleep_cycle is in half-cycles
|
||||
uint32_t rtc_freq = rtc_clk_slow_freq_get_hz();
|
||||
this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / (rtc_freq * 2);
|
||||
if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) {
|
||||
this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS;
|
||||
}
|
||||
// Check for releases at 1/4 the timeout interval, capped at 50ms
|
||||
this->release_check_interval_ms_ = std::min(this->release_timeout_ms_ / 4, (uint32_t) 50);
|
||||
|
||||
// Enable touch pad interrupt
|
||||
touch_pad_intr_enable();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
this->dump_config_base_();
|
||||
|
||||
if (this->iir_filter_enabled_()) {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter DISABLED");
|
||||
}
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
this->dump_config_sensors_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Print debug info for all pads in setup mode
|
||||
if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) {
|
||||
for (auto *child : this->children_) {
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(),
|
||||
(uint32_t) child->get_touch_pad(), child->value_);
|
||||
}
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
}
|
||||
|
||||
// Process any queued touch events from interrupts
|
||||
// Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0)
|
||||
// This is more efficient than sending all pad states every interrupt
|
||||
TouchPadEventV1 event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
// Find the corresponding sensor - O(n) search is acceptable since events are infrequent
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_touch_pad() != event.pad) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found matching pad - process it
|
||||
child->value_ = event.value;
|
||||
|
||||
// The interrupt gives us the touch state directly
|
||||
bool new_state = event.is_touched;
|
||||
|
||||
// Track when we last saw this pad as touched
|
||||
if (new_state) {
|
||||
this->last_touch_time_[event.pad] = now;
|
||||
}
|
||||
|
||||
// Only publish if state changed - this filters out repeated events
|
||||
if (new_state != child->last_state_) {
|
||||
child->last_state_ = new_state;
|
||||
child->publish_state(new_state);
|
||||
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
||||
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), event.value, child->get_threshold());
|
||||
}
|
||||
break; // Exit inner loop after processing matching pad
|
||||
}
|
||||
}
|
||||
|
||||
// Check for released pads periodically
|
||||
static uint32_t last_release_check = 0;
|
||||
if (now - last_release_check < this->release_check_interval_ms_) {
|
||||
return;
|
||||
}
|
||||
last_release_check = now;
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
touch_pad_t pad = child->get_touch_pad();
|
||||
uint32_t last_time = this->last_touch_time_[pad];
|
||||
|
||||
// Design note: Sentinel value pattern explanation
|
||||
// - 0: Never touched since boot (waiting for initial timeout)
|
||||
// - 1: Initial OFF state has been published (prevents repeated publishes)
|
||||
// - >1: Actual timestamp of last touch event
|
||||
// This avoids needing a separate boolean flag for initial state tracking
|
||||
|
||||
// If we've never seen this pad touched (last_time == 0) and enough time has passed
|
||||
// since startup, publish OFF state and mark as published with value 1
|
||||
if (last_time == 0 && now > this->release_timeout_ms_) {
|
||||
child->publish_initial_state(false);
|
||||
this->last_touch_time_[pad] = 1; // Mark as "initial state published"
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str());
|
||||
} else if (child->last_state_ && last_time > 1) { // last_time > 1 means it's a real timestamp
|
||||
uint32_t time_diff = now - last_time;
|
||||
|
||||
// Check if we haven't seen this pad recently
|
||||
if (time_diff > this->release_timeout_ms_) {
|
||||
// Haven't seen this pad recently, assume it's released
|
||||
child->last_state_ = false;
|
||||
child->publish_state(false);
|
||||
this->last_touch_time_[pad] = 1; // Reset to "initial published" state
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
touch_pad_intr_disable();
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_stop();
|
||||
touch_pad_filter_delete();
|
||||
}
|
||||
|
||||
// Configure wakeup pads if any are set
|
||||
this->configure_wakeup_pads_();
|
||||
}
|
||||
|
||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||
|
||||
touch_pad_clear_status();
|
||||
|
||||
// Process all configured pads to check their current state
|
||||
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
||||
// so we must scan all configured pads to find which ones were touched
|
||||
for (auto *child : component->children_) {
|
||||
touch_pad_t pad = child->get_touch_pad();
|
||||
|
||||
// Read current value using ISR-safe API
|
||||
uint32_t value;
|
||||
if (component->iir_filter_enabled_()) {
|
||||
uint16_t temp_value = 0;
|
||||
touch_pad_read_filtered(pad, &temp_value);
|
||||
value = temp_value;
|
||||
} else {
|
||||
// Use low-level HAL function when filter is not enabled
|
||||
value = touch_ll_read_raw_data(pad);
|
||||
}
|
||||
|
||||
// Skip pads with 0 value - they haven't been measured in this cycle
|
||||
// This is important: not all pads are measured every interrupt cycle,
|
||||
// only those that the hardware has updated
|
||||
if (value == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
||||
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
||||
// Therefore: touched = (value < threshold)
|
||||
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
||||
bool is_touched = value < child->get_threshold();
|
||||
|
||||
// Always send the current state - the main loop will filter for changes
|
||||
// We send both touched and untouched states because the ISR doesn't
|
||||
// track previous state (to keep ISR fast and simple)
|
||||
TouchPadEventV1 event;
|
||||
event.pad = pad;
|
||||
event.value = value;
|
||||
event.is_touched = is_touched;
|
||||
|
||||
// Send to queue from ISR - non-blocking, drops if queue full
|
||||
BaseType_t x_higher_priority_task_woken = pdFALSE;
|
||||
xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
|
||||
if (x_higher_priority_task_woken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
354
esphome/components/esp32_touch/esp32_touch_v2.cpp
Normal file
354
esphome/components/esp32_touch/esp32_touch_v2.cpp
Normal file
@@ -0,0 +1,354 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
// Helper to update touch state with a known state
|
||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) {
|
||||
if (child->last_state_ != is_touched) {
|
||||
// Read value for logging
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
|
||||
child->last_state_ = is_touched;
|
||||
child->publish_state(is_touched);
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' %s (value: %" PRIu32 " %s threshold: %" PRIu32 ")", child->get_name().c_str(),
|
||||
is_touched ? "touched" : "released", value, is_touched ? ">" : "<=", child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to read touch value and update state for a given child (used for timeout events)
|
||||
void ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) {
|
||||
// Read current touch value
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
|
||||
// ESP32-S2/S3 v2: Touch is detected when value > threshold
|
||||
bool is_touched = value > child->get_threshold();
|
||||
|
||||
this->update_touch_state_(child, is_touched);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events first
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize touch pad peripheral
|
||||
esp_err_t init_err = touch_pad_init();
|
||||
if (init_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure each touch pad first
|
||||
for (auto *child : this->children_) {
|
||||
esp_err_t config_err = touch_pad_config(child->get_touch_pad());
|
||||
if (config_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->get_touch_pad(), esp_err_to_name(config_err));
|
||||
}
|
||||
}
|
||||
|
||||
// Set up filtering if configured
|
||||
if (this->filter_configured_()) {
|
||||
touch_filter_config_t filter_info = {
|
||||
.mode = this->filter_mode_,
|
||||
.debounce_cnt = this->debounce_count_,
|
||||
.noise_thr = this->noise_threshold_,
|
||||
.jitter_step = this->jitter_step_,
|
||||
.smh_lvl = this->smooth_level_,
|
||||
};
|
||||
touch_pad_filter_set_config(&filter_info);
|
||||
touch_pad_filter_enable();
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
touch_pad_denoise_t denoise = {
|
||||
.grade = this->grade_,
|
||||
.cap_level = this->cap_level_,
|
||||
};
|
||||
touch_pad_denoise_set_config(&denoise);
|
||||
touch_pad_denoise_enable();
|
||||
}
|
||||
|
||||
if (this->waterproof_configured_()) {
|
||||
touch_pad_waterproof_t waterproof = {
|
||||
.guard_ring_pad = this->waterproof_guard_ring_pad_,
|
||||
.shield_driver = this->waterproof_shield_driver_,
|
||||
};
|
||||
touch_pad_waterproof_set_config(&waterproof);
|
||||
touch_pad_waterproof_enable();
|
||||
}
|
||||
|
||||
// Configure measurement parameters
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
// ESP32-S2/S3 always use the older API
|
||||
touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
|
||||
|
||||
// Configure timeout if needed
|
||||
touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX);
|
||||
|
||||
// Register ISR handler with interrupt mask
|
||||
esp_err_t err =
|
||||
touch_pad_isr_register(touch_isr_handler, this, static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ALL));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set thresholds for each pad BEFORE starting FSM
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_threshold() != 0) {
|
||||
touch_pad_set_thresh(child->get_touch_pad(), child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Enable interrupts
|
||||
touch_pad_intr_enable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_INACTIVE |
|
||||
TOUCH_PAD_INTR_MASK_TIMEOUT));
|
||||
|
||||
// Set FSM mode before starting
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
|
||||
// Start FSM
|
||||
touch_pad_fsm_start();
|
||||
|
||||
// Read initial states after all hardware is initialized
|
||||
for (auto *child : this->children_) {
|
||||
// Read current value
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
|
||||
// Set initial state and publish
|
||||
bool is_touched = value > child->get_threshold();
|
||||
child->last_state_ = is_touched;
|
||||
child->publish_initial_state(is_touched);
|
||||
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' initial state: %s (value: %d %s threshold: %d)", child->get_name().c_str(),
|
||||
is_touched ? "touched" : "released", value, is_touched ? ">" : "<=", child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
this->dump_config_base_();
|
||||
|
||||
if (this->filter_configured_()) {
|
||||
const char *filter_mode_s;
|
||||
switch (this->filter_mode_) {
|
||||
case TOUCH_PAD_FILTER_IIR_4:
|
||||
filter_mode_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_8:
|
||||
filter_mode_s = "IIR_8";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_16:
|
||||
filter_mode_s = "IIR_16";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_32:
|
||||
filter_mode_s = "IIR_32";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_64:
|
||||
filter_mode_s = "IIR_64";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_128:
|
||||
filter_mode_s = "IIR_128";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_256:
|
||||
filter_mode_s = "IIR_256";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_JITTER:
|
||||
filter_mode_s = "JITTER";
|
||||
break;
|
||||
default:
|
||||
filter_mode_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Filter mode: %s\n"
|
||||
" Debounce count: %" PRIu32 "\n"
|
||||
" Noise threshold coefficient: %" PRIu32 "\n"
|
||||
" Jitter filter step size: %" PRIu32,
|
||||
filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_);
|
||||
const char *smooth_level_s;
|
||||
switch (this->smooth_level_) {
|
||||
case TOUCH_PAD_SMOOTH_OFF:
|
||||
smooth_level_s = "OFF";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_2:
|
||||
smooth_level_s = "IIR_2";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_4:
|
||||
smooth_level_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_8:
|
||||
smooth_level_s = "IIR_8";
|
||||
break;
|
||||
default:
|
||||
smooth_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s);
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
const char *grade_s;
|
||||
switch (this->grade_) {
|
||||
case TOUCH_PAD_DENOISE_BIT12:
|
||||
grade_s = "BIT12";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT10:
|
||||
grade_s = "BIT10";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT8:
|
||||
grade_s = "BIT8";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT4:
|
||||
grade_s = "BIT4";
|
||||
break;
|
||||
default:
|
||||
grade_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s);
|
||||
|
||||
const char *cap_level_s;
|
||||
switch (this->cap_level_) {
|
||||
case TOUCH_PAD_DENOISE_CAP_L0:
|
||||
cap_level_s = "L0";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L1:
|
||||
cap_level_s = "L1";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L2:
|
||||
cap_level_s = "L2";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L3:
|
||||
cap_level_s = "L3";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L4:
|
||||
cap_level_s = "L4";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L5:
|
||||
cap_level_s = "L5";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L6:
|
||||
cap_level_s = "L6";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L7:
|
||||
cap_level_s = "L7";
|
||||
break;
|
||||
default:
|
||||
cap_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s);
|
||||
}
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
this->dump_config_sensors_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// In setup mode, periodically log all pad values
|
||||
if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) {
|
||||
for (auto *child : this->children_) {
|
||||
// Read the value being used for touch detection
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value);
|
||||
}
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
}
|
||||
|
||||
// Process any queued touch events from interrupts
|
||||
TouchPadEventV2 event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
// Handle timeout events
|
||||
if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
|
||||
// Resume measurement after timeout
|
||||
touch_pad_timeout_resume();
|
||||
// For timeout events, always check the current state
|
||||
} else if (!(event.intr_mask & (TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_INACTIVE))) {
|
||||
// Skip if not an active/inactive/timeout event
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the child for the pad that triggered the interrupt
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_touch_pad() != event.pad) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
|
||||
// For timeout events, we need to read the value to determine state
|
||||
this->check_and_update_touch_state_(child);
|
||||
} else {
|
||||
// For ACTIVE/INACTIVE events, the interrupt tells us the state
|
||||
bool is_touched = (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) != 0;
|
||||
this->update_touch_state_(child, is_touched);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
// Disable interrupts
|
||||
touch_pad_intr_disable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_INACTIVE |
|
||||
TOUCH_PAD_INTR_MASK_TIMEOUT));
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
// Configure wakeup pads if any are set
|
||||
this->configure_wakeup_pads_();
|
||||
}
|
||||
|
||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||
BaseType_t x_higher_priority_task_woken = pdFALSE;
|
||||
|
||||
// Read interrupt status
|
||||
TouchPadEventV2 event;
|
||||
event.intr_mask = touch_pad_read_intr_status_mask();
|
||||
event.pad = touch_pad_get_current_meas_channel();
|
||||
|
||||
// Send event to queue for processing in main loop
|
||||
xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
|
||||
|
||||
if (x_higher_priority_task_woken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const {
|
||||
// Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations.
|
||||
// The hardware continuously samples in the background and we can read the
|
||||
// latest value at any time without waiting.
|
||||
uint32_t value = 0;
|
||||
if (this->filter_configured_()) {
|
||||
// Read filtered/smoothed value when filter is enabled
|
||||
touch_pad_filter_read_smooth(pad, &value);
|
||||
} else {
|
||||
// Read raw value when filter is not configured
|
||||
touch_pad_read_raw_data(pad, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
|
||||
@@ -50,7 +50,7 @@ MCP23016_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
cv.int_range(min=0, max=15),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT],
|
||||
mode_validator=validate_mode,
|
||||
invertable=True,
|
||||
invertible=True,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_MCP23016): cv.use_id(MCP23016),
|
||||
|
||||
@@ -60,7 +60,7 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
cv.int_range(min=0, max=15),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP],
|
||||
mode_validator=validate_mode,
|
||||
invertable=True,
|
||||
invertible=True,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase),
|
||||
|
||||
@@ -53,7 +53,7 @@ PCF8574_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
cv.int_range(min=0, max=17),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT],
|
||||
mode_validator=validate_mode,
|
||||
invertable=True,
|
||||
invertible=True,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component),
|
||||
|
||||
26
esphome/components/runtime_stats/__init__.py
Normal file
26
esphome/components/runtime_stats/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Runtime statistics component for ESPHome.
|
||||
"""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_LOG_INTERVAL = "log_interval"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_LOG_INTERVAL, default=60000
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the runtime statistics component."""
|
||||
cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
|
||||
cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))
|
||||
@@ -95,7 +95,7 @@ SN74HC595_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
cv.int_range(min=0, max=2047),
|
||||
modes=[CONF_OUTPUT],
|
||||
mode_validator=_validate_output_mode,
|
||||
invertable=True,
|
||||
invertible=True,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component),
|
||||
|
||||
@@ -53,7 +53,7 @@ TCA9555_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
cv.int_range(min=0, max=15),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT],
|
||||
mode_validator=validate_mode,
|
||||
invertable=True,
|
||||
invertible=True,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component),
|
||||
|
||||
@@ -6,16 +6,8 @@ namespace template_ {
|
||||
|
||||
static const char *const TAG = "template.binary_sensor";
|
||||
|
||||
void TemplateBinarySensor::setup() {
|
||||
if (!this->publish_initial_state_)
|
||||
return;
|
||||
void TemplateBinarySensor::setup() { this->loop(); }
|
||||
|
||||
if (this->f_ != nullptr) {
|
||||
this->publish_initial_state(this->f_().value_or(false));
|
||||
} else {
|
||||
this->publish_initial_state(false);
|
||||
}
|
||||
}
|
||||
void TemplateBinarySensor::loop() {
|
||||
if (this->f_ == nullptr)
|
||||
return;
|
||||
|
||||
@@ -555,7 +555,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
|
||||
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator);
|
||||
|
||||
@@ -269,7 +269,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
|
||||
|
||||
/// Handle a binary sensor request under '/binary_sensor/<id>'.
|
||||
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Constants used by esphome."""
|
||||
|
||||
__version__ = "2025.6.0b1"
|
||||
__version__ = "2025.7.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -117,7 +117,9 @@ void Application::loop() {
|
||||
// Use the last component's end time instead of calling millis() again
|
||||
auto elapsed = last_op_end_time - this->last_loop_;
|
||||
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
|
||||
yield();
|
||||
// Even if we overran the loop interval, we still need to select()
|
||||
// to know if any sockets have data ready
|
||||
this->yield_with_select_(0);
|
||||
} else {
|
||||
uint32_t delay_time = this->loop_interval_ - elapsed;
|
||||
uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time);
|
||||
@@ -126,7 +128,7 @@ void Application::loop() {
|
||||
next_schedule = std::max(next_schedule, delay_time / 2);
|
||||
delay_time = std::min(next_schedule, delay_time);
|
||||
|
||||
this->delay_with_select_(delay_time);
|
||||
this->yield_with_select_(delay_time);
|
||||
}
|
||||
this->last_loop_ = last_op_end_time;
|
||||
|
||||
@@ -215,7 +217,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
|
||||
|
||||
// Give some time for I/O operations if components are still pending
|
||||
if (!pending_components.empty()) {
|
||||
this->delay_with_select_(1);
|
||||
this->yield_with_select_(1);
|
||||
}
|
||||
|
||||
// Update time for next iteration
|
||||
@@ -293,8 +295,6 @@ bool Application::is_socket_ready(int fd) const {
|
||||
// This function is thread-safe for reading the result of select()
|
||||
// However, it should only be called after select() has been executed in the main loop
|
||||
// The read_fds_ is only modified by select() in the main loop
|
||||
if (HighFrequencyLoopRequester::is_high_frequency())
|
||||
return true; // fd sets via select are not updated in high frequency looping - so force true fallback behavior
|
||||
if (fd < 0 || fd >= FD_SETSIZE)
|
||||
return false;
|
||||
|
||||
@@ -302,7 +302,9 @@ bool Application::is_socket_ready(int fd) const {
|
||||
}
|
||||
#endif
|
||||
|
||||
void Application::delay_with_select_(uint32_t delay_ms) {
|
||||
void Application::yield_with_select_(uint32_t delay_ms) {
|
||||
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run
|
||||
// since select() with 0 timeout only polls without yielding.
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
if (!this->socket_fds_.empty()) {
|
||||
// Update fd_set if socket list has changed
|
||||
@@ -340,6 +342,10 @@ void Application::delay_with_select_(uint32_t delay_ms) {
|
||||
ESP_LOGW(TAG, "select() failed with errno %d", errno);
|
||||
delay(delay_ms);
|
||||
}
|
||||
// When delay_ms is 0, we need to yield since select(0) doesn't yield
|
||||
if (delay_ms == 0) {
|
||||
yield();
|
||||
}
|
||||
} else {
|
||||
// No sockets registered, use regular delay
|
||||
delay(delay_ms);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/runtime_stats.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
@@ -314,6 +315,18 @@ class Application {
|
||||
|
||||
uint32_t get_loop_interval() const { return this->loop_interval_; }
|
||||
|
||||
/** Enable or disable runtime statistics collection.
|
||||
*
|
||||
* @param enable Whether to enable runtime statistics collection.
|
||||
*/
|
||||
void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
|
||||
|
||||
/** Set the interval at which runtime statistics are logged.
|
||||
*
|
||||
* @param interval The interval in milliseconds between logging of runtime statistics.
|
||||
*/
|
||||
void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
|
||||
|
||||
void schedule_dump_config() { this->dump_config_at_ = 0; }
|
||||
|
||||
void feed_wdt(uint32_t time = 0);
|
||||
@@ -575,7 +588,7 @@ class Application {
|
||||
void feed_wdt_arch_();
|
||||
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
void delay_with_select_(uint32_t delay_ms);
|
||||
void yield_with_select_(uint32_t delay_ms);
|
||||
|
||||
std::vector<Component *> components_{};
|
||||
std::vector<Component *> looping_components_{};
|
||||
|
||||
@@ -246,6 +246,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
||||
uint32_t curr_time = millis();
|
||||
|
||||
uint32_t blocking_time = curr_time - this->started_;
|
||||
|
||||
// Record component runtime stats
|
||||
runtime_stats.record_component_time(this->component_, blocking_time, curr_time);
|
||||
bool should_warn;
|
||||
if (this->component_ != nullptr) {
|
||||
should_warn = this->component_->should_warn_of_blocking(blocking_time);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/runtime_stats.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ namespace esphome {
|
||||
void Controller::setup_controller(bool include_internal) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (auto *obj : App.get_binary_sensors()) {
|
||||
if (include_internal || !obj->is_internal())
|
||||
obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); });
|
||||
if (include_internal || !obj->is_internal()) {
|
||||
obj->add_full_state_callback(
|
||||
[this, obj](optional<bool> previous, optional<bool> state) { this->on_binary_sensor_update(obj); });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
|
||||
@@ -71,7 +71,7 @@ class Controller {
|
||||
public:
|
||||
void setup_controller(bool include_internal = false);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){};
|
||||
virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){};
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
virtual void on_fan_update(fan::Fan *obj){};
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include "string_ref.h"
|
||||
#include "helpers.h"
|
||||
#include "log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -29,7 +31,7 @@ class EntityBase {
|
||||
// Get the unique Object ID of this Entity
|
||||
uint32_t get_object_id_hash();
|
||||
|
||||
// Get/set whether this Entity should be hidden from outside of ESPHome
|
||||
// Get/set whether this Entity should be hidden outside ESPHome
|
||||
bool is_internal() const;
|
||||
void set_internal(bool internal);
|
||||
|
||||
@@ -56,11 +58,12 @@ class EntityBase {
|
||||
StringRef name_;
|
||||
const char *object_id_c_str_{nullptr};
|
||||
const char *icon_c_str_{nullptr};
|
||||
uint32_t object_id_hash_;
|
||||
uint32_t object_id_hash_{};
|
||||
bool has_own_name_{false};
|
||||
bool internal_{false};
|
||||
bool disabled_by_default_{false};
|
||||
EntityCategory entity_category_{ENTITY_CATEGORY_NONE};
|
||||
bool has_state_{};
|
||||
};
|
||||
|
||||
class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming)
|
||||
@@ -85,4 +88,58 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
|
||||
const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override
|
||||
};
|
||||
|
||||
/**
|
||||
* An entity that has a state.
|
||||
* @tparam T The type of the state
|
||||
*/
|
||||
template<typename T> class StatefulEntityBase : public EntityBase {
|
||||
public:
|
||||
virtual bool has_state() const { return this->state_.has_value(); }
|
||||
virtual const T &get_state() const { return this->state_.value(); }
|
||||
virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); }
|
||||
void invalidate_state() { this->set_state_({}); }
|
||||
|
||||
void add_full_state_callback(std::function<void(optional<T> previous, optional<T> current)> &&callback) {
|
||||
if (this->full_state_callbacks_ == nullptr)
|
||||
this->full_state_callbacks_ = new CallbackManager<void(optional<T> previous, optional<T> current)>(); // NOLINT
|
||||
this->full_state_callbacks_->add(std::move(callback));
|
||||
}
|
||||
void add_on_state_callback(std::function<void(T)> &&callback) {
|
||||
if (this->state_callbacks_ == nullptr)
|
||||
this->state_callbacks_ = new CallbackManager<void(T)>(); // NOLINT
|
||||
this->state_callbacks_->add(std::move(callback));
|
||||
}
|
||||
|
||||
void set_trigger_on_initial_state(bool trigger_on_initial_state) {
|
||||
this->trigger_on_initial_state_ = trigger_on_initial_state;
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<T> state_{};
|
||||
/**
|
||||
* Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous.
|
||||
*
|
||||
* @param state The new state.
|
||||
* @return True if the state was changed, false if it was the same as before.
|
||||
*/
|
||||
bool set_state_(const optional<T> &state) {
|
||||
if (this->state_ != state) {
|
||||
// call the full state callbacks with the previous and new state
|
||||
if (this->full_state_callbacks_ != nullptr)
|
||||
this->full_state_callbacks_->call(this->state_, state);
|
||||
// trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or
|
||||
// the previous state was valid
|
||||
auto had_state = this->has_state();
|
||||
this->state_ = state;
|
||||
if (this->state_callbacks_ != nullptr && state.has_value() && (this->trigger_on_initial_state_ || had_state))
|
||||
this->state_callbacks_->call(state.value());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool trigger_on_initial_state_{true};
|
||||
// callbacks with full state and previous state
|
||||
CallbackManager<void(optional<T> previous, optional<T> current)> *full_state_callbacks_{};
|
||||
CallbackManager<void(T)> *state_callbacks_{};
|
||||
};
|
||||
} // namespace esphome
|
||||
|
||||
@@ -165,6 +165,8 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
#define YESNO(b) ((b) ? "YES" : "NO")
|
||||
#define ONOFF(b) ((b) ? "ON" : "OFF")
|
||||
#define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE")
|
||||
// for use with optional values
|
||||
#define ONOFFMAYBE(b) (((b).has_value()) ? ONOFF((b).value()) : "UNKNOWN")
|
||||
|
||||
// Helper class that identifies strings that may be stored in flash storage (similar to Arduino's __FlashStringHelper)
|
||||
struct LogString;
|
||||
|
||||
@@ -52,6 +52,11 @@ template<typename T> class optional { // NOLINT
|
||||
reset();
|
||||
return *this;
|
||||
}
|
||||
bool operator==(optional<T> const &rhs) const {
|
||||
if (has_value() && rhs.has_value())
|
||||
return value() == rhs.value();
|
||||
return !has_value() && !rhs.has_value();
|
||||
}
|
||||
|
||||
template<class U> optional &operator=(optional<U> const &other) {
|
||||
has_value_ = other.has_value();
|
||||
|
||||
28
esphome/core/runtime_stats.cpp
Normal file
28
esphome/core/runtime_stats.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "esphome/core/runtime_stats.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
RuntimeStatsCollector runtime_stats;
|
||||
|
||||
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
|
||||
if (!this->enabled_ || component == nullptr)
|
||||
return;
|
||||
|
||||
const char *component_source = component->get_component_source();
|
||||
this->component_stats_[component_source].record_time(duration_ms);
|
||||
|
||||
// If next_log_time_ is 0, initialize it
|
||||
if (this->next_log_time_ == 0) {
|
||||
this->next_log_time_ = current_time + this->log_interval_;
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_time >= this->next_log_time_) {
|
||||
this->log_stats_();
|
||||
this->reset_stats_();
|
||||
this->next_log_time_ = current_time + this->log_interval_;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
161
esphome/core/runtime_stats.h
Normal file
161
esphome/core/runtime_stats.h
Normal file
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
static const char *const RUNTIME_TAG = "runtime";
|
||||
|
||||
class Component; // Forward declaration
|
||||
|
||||
class ComponentRuntimeStats {
|
||||
public:
|
||||
ComponentRuntimeStats()
|
||||
: period_count_(0),
|
||||
total_count_(0),
|
||||
period_time_ms_(0),
|
||||
total_time_ms_(0),
|
||||
period_max_time_ms_(0),
|
||||
total_max_time_ms_(0) {}
|
||||
|
||||
void record_time(uint32_t duration_ms) {
|
||||
// Update period counters
|
||||
this->period_count_++;
|
||||
this->period_time_ms_ += duration_ms;
|
||||
if (duration_ms > this->period_max_time_ms_)
|
||||
this->period_max_time_ms_ = duration_ms;
|
||||
|
||||
// Update total counters
|
||||
this->total_count_++;
|
||||
this->total_time_ms_ += duration_ms;
|
||||
if (duration_ms > this->total_max_time_ms_)
|
||||
this->total_max_time_ms_ = duration_ms;
|
||||
}
|
||||
|
||||
void reset_period_stats() {
|
||||
this->period_count_ = 0;
|
||||
this->period_time_ms_ = 0;
|
||||
this->period_max_time_ms_ = 0;
|
||||
}
|
||||
|
||||
// Period stats (reset each logging interval)
|
||||
uint32_t get_period_count() const { return this->period_count_; }
|
||||
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
|
||||
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
|
||||
float get_period_avg_time_ms() const {
|
||||
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
|
||||
}
|
||||
|
||||
// Total stats (persistent until reboot)
|
||||
uint32_t get_total_count() const { return this->total_count_; }
|
||||
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
|
||||
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
|
||||
float get_total_avg_time_ms() const {
|
||||
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Period stats (reset each logging interval)
|
||||
uint32_t period_count_;
|
||||
uint32_t period_time_ms_;
|
||||
uint32_t period_max_time_ms_;
|
||||
|
||||
// Total stats (persistent until reboot)
|
||||
uint32_t total_count_;
|
||||
uint32_t total_time_ms_;
|
||||
uint32_t total_max_time_ms_;
|
||||
};
|
||||
|
||||
// For sorting components by run time
|
||||
struct ComponentStatPair {
|
||||
std::string name;
|
||||
const ComponentRuntimeStats *stats;
|
||||
|
||||
bool operator>(const ComponentStatPair &other) const {
|
||||
// Sort by period time as that's what we're displaying in the logs
|
||||
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
|
||||
}
|
||||
};
|
||||
|
||||
class RuntimeStatsCollector {
|
||||
public:
|
||||
RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
|
||||
|
||||
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
|
||||
uint32_t get_log_interval() const { return this->log_interval_; }
|
||||
|
||||
void set_enabled(bool enabled) { this->enabled_ = enabled; }
|
||||
bool is_enabled() const { return this->enabled_; }
|
||||
|
||||
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
|
||||
|
||||
protected:
|
||||
void log_stats_() {
|
||||
ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
|
||||
ESP_LOGI(RUNTIME_TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
|
||||
|
||||
// First collect stats we want to display
|
||||
std::vector<ComponentStatPair> stats_to_display;
|
||||
|
||||
for (const auto &it : this->component_stats_) {
|
||||
const ComponentRuntimeStats &stats = it.second;
|
||||
if (stats.get_period_count() > 0) {
|
||||
ComponentStatPair pair = {it.first, &stats};
|
||||
stats_to_display.push_back(pair);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
|
||||
|
||||
// Log top components by period runtime
|
||||
for (const auto &it : stats_to_display) {
|
||||
const std::string &source = it.name;
|
||||
const ComponentRuntimeStats *stats = it.stats;
|
||||
|
||||
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
|
||||
source.c_str(), stats->get_period_count(), stats->get_period_avg_time_ms(),
|
||||
stats->get_period_max_time_ms(), stats->get_period_time_ms());
|
||||
}
|
||||
|
||||
// Log total stats since boot
|
||||
ESP_LOGI(RUNTIME_TAG, "Total stats (since boot):");
|
||||
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(stats_to_display.begin(), stats_to_display.end(),
|
||||
[](const ComponentStatPair &a, const ComponentStatPair &b) {
|
||||
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
|
||||
});
|
||||
|
||||
for (const auto &it : stats_to_display) {
|
||||
const std::string &source = it.name;
|
||||
const ComponentRuntimeStats *stats = it.stats;
|
||||
|
||||
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
|
||||
source.c_str(), stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
|
||||
stats->get_total_time_ms());
|
||||
}
|
||||
}
|
||||
|
||||
void reset_stats_() {
|
||||
for (auto &it : this->component_stats_) {
|
||||
it.second.reset_period_stats();
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, ComponentRuntimeStats> component_stats_;
|
||||
uint32_t log_interval_;
|
||||
uint32_t next_log_time_;
|
||||
bool enabled_;
|
||||
};
|
||||
|
||||
// Global instance for runtime stats collection
|
||||
extern RuntimeStatsCollector runtime_stats;
|
||||
|
||||
} // namespace esphome
|
||||
@@ -1,5 +1,8 @@
|
||||
from collections.abc import Callable
|
||||
from functools import reduce
|
||||
from logging import Logger
|
||||
import operator
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -15,6 +18,7 @@ from esphome.const import (
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
|
||||
class PinRegistry(dict):
|
||||
@@ -262,7 +266,7 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator(
|
||||
)
|
||||
|
||||
|
||||
def check_strapping_pin(conf, strapping_pin_list, logger):
|
||||
def check_strapping_pin(conf, strapping_pin_list: set[int], logger: Logger):
|
||||
num = conf[CONF_NUMBER]
|
||||
if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING):
|
||||
logger.warning(
|
||||
@@ -291,11 +295,11 @@ def gpio_validate_modes(value):
|
||||
|
||||
|
||||
def gpio_base_schema(
|
||||
pin_type,
|
||||
number_validator,
|
||||
pin_type: MockObjClass,
|
||||
number_validator: Callable[[Any], Any],
|
||||
modes=GPIO_STANDARD_MODES,
|
||||
mode_validator=gpio_validate_modes,
|
||||
invertable=True,
|
||||
mode_validator: Callable[[Any], Any] = gpio_validate_modes,
|
||||
invertible: bool = True,
|
||||
):
|
||||
"""
|
||||
Generate a base gpio pin schema
|
||||
@@ -303,7 +307,7 @@ def gpio_base_schema(
|
||||
:param number_validator: A validator for the pin number
|
||||
:param modes: The available modes, default is all standard modes
|
||||
:param mode_validator: A validator function for the pin mode
|
||||
:param invertable: If the pin supports hardware inversion
|
||||
:param invertible: If the pin supports hardware inversion
|
||||
:return: A schema for the pin
|
||||
"""
|
||||
mode_default = len(modes) == 1
|
||||
@@ -328,7 +332,7 @@ def gpio_base_schema(
|
||||
}
|
||||
)
|
||||
|
||||
if invertable:
|
||||
if invertible:
|
||||
return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean})
|
||||
|
||||
return schema
|
||||
|
||||
@@ -6,7 +6,7 @@ pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==8.4.0
|
||||
pytest-cov==6.1.1
|
||||
pytest-cov==6.2.1
|
||||
pytest-mock==3.14.1
|
||||
pytest-asyncio==0.26.0
|
||||
pytest-xdist==3.7.0
|
||||
|
||||
15
tests/components/binary_sensor/common.yaml
Normal file
15
tests/components/binary_sensor/common.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
trigger_on_initial_state: true
|
||||
id: some_binary_sensor
|
||||
name: "Random binary"
|
||||
lambda: return (random_uint32() & 1) == 0;
|
||||
on_state_change:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Old state was %s"
|
||||
args: ['x_previous.has_value() ? ONOFF(x_previous) : "Unknown"']
|
||||
- logger.log:
|
||||
format: "New state is %s"
|
||||
args: ['x.has_value() ? ONOFF(x) : "Unknown"']
|
||||
- binary_sensor.invalidate_state: some_binary_sensor
|
||||
2
tests/components/binary_sensor/test.bk72xx-ard.yaml
Normal file
2
tests/components/binary_sensor/test.bk72xx-ard.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.esp32-ard.yaml
Normal file
2
tests/components/binary_sensor/test.esp32-ard.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.esp32-c3-ard.yaml
Normal file
2
tests/components/binary_sensor/test.esp32-c3-ard.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.esp32-c3-idf.yaml
Normal file
2
tests/components/binary_sensor/test.esp32-c3-idf.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.esp32-idf.yaml
Normal file
2
tests/components/binary_sensor/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.esp32-s3-idf.yaml
Normal file
2
tests/components/binary_sensor/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.esp8266-ard.yaml
Normal file
2
tests/components/binary_sensor/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
2
tests/components/binary_sensor/test.rp2040-ard.yaml
Normal file
2
tests/components/binary_sensor/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
@@ -63,7 +63,7 @@ binary_sensor:
|
||||
id: lvgl_pressbutton
|
||||
name: Pressbutton
|
||||
widget: spin_up
|
||||
publish_initial_state: true
|
||||
trigger_on_initial_state: true
|
||||
- platform: lvgl
|
||||
name: ButtonMatrix button
|
||||
widget: button_a
|
||||
|
||||
Reference in New Issue
Block a user