mirror of
https://github.com/esphome/esphome.git
synced 2025-11-17 07:15:48 +00:00
Compare commits
4 Commits
integratio
...
bh1750_loo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78a69cb744 | ||
|
|
9b14444dad | ||
|
|
8934d4b498 | ||
|
|
9b107e7f2a |
@@ -741,13 +741,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Set memory analysis options in config
|
||||
if args.analyze_memory:
|
||||
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
|
||||
|
||||
if args.memory_report:
|
||||
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
|
||||
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -1209,17 +1202,6 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--analyze-memory",
|
||||
help="Analyze and display memory usage by component after compilation.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--memory-report",
|
||||
help="Save memory analysis report to a file (supports .json or .txt).",
|
||||
type=str,
|
||||
metavar="FILE",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
|
||||
@@ -51,14 +51,13 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
@@ -96,14 +95,13 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "bh1750.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bh1750 {
|
||||
namespace esphome::bh1750 {
|
||||
|
||||
static const char *const TAG = "bh1750.sensor";
|
||||
|
||||
@@ -13,6 +13,31 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
|
||||
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
|
||||
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
|
||||
|
||||
static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000;
|
||||
static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f;
|
||||
|
||||
// Measurement time constants (datasheet values)
|
||||
static constexpr uint16_t MTREG_DEFAULT = 69;
|
||||
static constexpr uint16_t MTREG_MIN = 31;
|
||||
static constexpr uint16_t MTREG_MAX = 254;
|
||||
static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69
|
||||
static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69
|
||||
|
||||
// Conversion constants (datasheet formulas)
|
||||
static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor
|
||||
static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution
|
||||
|
||||
// MTreg calculation constants
|
||||
static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation)
|
||||
static constexpr int COUNTS_NUMERATOR = 10;
|
||||
static constexpr int COUNTS_DENOMINATOR = 12;
|
||||
|
||||
// MTreg register bit manipulation constants
|
||||
static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5
|
||||
static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits
|
||||
static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0
|
||||
static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits
|
||||
|
||||
/*
|
||||
bh1750 properties:
|
||||
|
||||
@@ -43,74 +68,7 @@ void BH1750Sensor::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
|
||||
// turn on (after one-shot sensor automatically powers down)
|
||||
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
|
||||
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Power on failed");
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (active_mtreg_ != mtreg) {
|
||||
// set mtreg
|
||||
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
|
||||
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
|
||||
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Set measurement time failed");
|
||||
active_mtreg_ = 0;
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
active_mtreg_ = mtreg;
|
||||
}
|
||||
|
||||
uint8_t cmd;
|
||||
uint16_t meas_time;
|
||||
switch (mode) {
|
||||
case BH1750_MODE_L:
|
||||
cmd = BH1750_COMMAND_ONE_TIME_L;
|
||||
meas_time = 24 * mtreg / 69;
|
||||
break;
|
||||
case BH1750_MODE_H:
|
||||
cmd = BH1750_COMMAND_ONE_TIME_H;
|
||||
meas_time = 180 * mtreg / 69;
|
||||
break;
|
||||
case BH1750_MODE_H2:
|
||||
cmd = BH1750_COMMAND_ONE_TIME_H2;
|
||||
meas_time = 180 * mtreg / 69;
|
||||
break;
|
||||
default:
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Start measurement failed");
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
|
||||
// probably not needed, but adjust for rounding
|
||||
meas_time++;
|
||||
|
||||
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
|
||||
uint16_t raw_value;
|
||||
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Read data failed");
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
raw_value = i2c::i2ctohs(raw_value);
|
||||
|
||||
float lx = float(raw_value) / 1.2f;
|
||||
lx *= 69.0f / mtreg;
|
||||
if (mode == BH1750_MODE_H2)
|
||||
lx /= 2.0f;
|
||||
|
||||
f(lx);
|
||||
});
|
||||
this->state_ = IDLE;
|
||||
}
|
||||
|
||||
void BH1750Sensor::dump_config() {
|
||||
@@ -124,45 +82,188 @@ void BH1750Sensor::dump_config() {
|
||||
}
|
||||
|
||||
void BH1750Sensor::update() {
|
||||
// first do a quick measurement in L-mode with full range
|
||||
// to find right range
|
||||
this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
|
||||
if (std::isnan(val)) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
const uint32_t now = millis();
|
||||
|
||||
// Start coarse measurement to determine optimal mode/mtreg
|
||||
if (this->state_ != IDLE) {
|
||||
// Safety timeout: reset if stuck
|
||||
if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Measurement timeout, resetting state");
|
||||
this->state_ = IDLE;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Previous measurement not complete, skipping update");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BH1750Mode use_mode;
|
||||
uint8_t use_mtreg;
|
||||
if (val <= 7000) {
|
||||
use_mode = BH1750_MODE_H2;
|
||||
use_mtreg = 254;
|
||||
} else {
|
||||
use_mode = BH1750_MODE_H;
|
||||
// lx = counts / 1.2 * (69 / mtreg)
|
||||
// -> mtreg = counts / 1.2 * (69 / lx)
|
||||
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
|
||||
// -> mtreg = 50000*(10/12)*(69/lx)
|
||||
int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
|
||||
use_mtreg = std::min(254, std::max(31, ideal_mtreg));
|
||||
}
|
||||
ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
|
||||
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
|
||||
this->read_lx_(use_mode, use_mtreg, [this](float val) {
|
||||
if (std::isnan(val)) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
this->state_ = WAITING_COARSE_MEASUREMENT;
|
||||
this->enable_loop(); // Enable loop while measurement in progress
|
||||
}
|
||||
|
||||
void BH1750Sensor::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
switch (this->state_) {
|
||||
case IDLE:
|
||||
// Disable loop when idle to save cycles
|
||||
this->disable_loop();
|
||||
break;
|
||||
|
||||
case WAITING_COARSE_MEASUREMENT:
|
||||
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
|
||||
this->state_ = READING_COARSE_RESULT;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
|
||||
break;
|
||||
|
||||
case READING_COARSE_RESULT: {
|
||||
float lx;
|
||||
if (!this->read_measurement_(lx)) {
|
||||
this->fail_and_reset_();
|
||||
break;
|
||||
}
|
||||
|
||||
this->process_coarse_result_(lx);
|
||||
|
||||
// Start fine measurement with optimal settings
|
||||
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, now)) {
|
||||
this->fail_and_reset_();
|
||||
break;
|
||||
}
|
||||
|
||||
this->state_ = WAITING_FINE_MEASUREMENT;
|
||||
break;
|
||||
}
|
||||
|
||||
case WAITING_FINE_MEASUREMENT:
|
||||
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
|
||||
this->state_ = READING_FINE_RESULT;
|
||||
}
|
||||
break;
|
||||
|
||||
case READING_FINE_RESULT: {
|
||||
float lx;
|
||||
if (!this->read_measurement_(lx)) {
|
||||
this->fail_and_reset_();
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
|
||||
this->status_clear_warning();
|
||||
this->publish_state(val);
|
||||
});
|
||||
});
|
||||
this->publish_state(lx);
|
||||
this->state_ = IDLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) {
|
||||
// Power on
|
||||
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
|
||||
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Power on failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set MTreg if changed
|
||||
if (this->active_mtreg_ != mtreg) {
|
||||
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK);
|
||||
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK);
|
||||
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Set measurement time failed");
|
||||
this->active_mtreg_ = 0;
|
||||
return false;
|
||||
}
|
||||
this->active_mtreg_ = mtreg;
|
||||
}
|
||||
|
||||
// Start measurement
|
||||
uint8_t cmd;
|
||||
uint16_t meas_time;
|
||||
switch (mode) {
|
||||
case BH1750_MODE_L:
|
||||
cmd = BH1750_COMMAND_ONE_TIME_L;
|
||||
meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT;
|
||||
break;
|
||||
case BH1750_MODE_H:
|
||||
cmd = BH1750_COMMAND_ONE_TIME_H;
|
||||
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
|
||||
break;
|
||||
case BH1750_MODE_H2:
|
||||
cmd = BH1750_COMMAND_ONE_TIME_H2;
|
||||
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Start measurement failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store current measurement parameters
|
||||
this->current_mode_ = mode;
|
||||
this->current_mtreg_ = mtreg;
|
||||
this->measurement_start_time_ = now;
|
||||
this->measurement_duration_ = meas_time + 1; // Add 1ms for safety
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BH1750Sensor::read_measurement_(float &lx_out) {
|
||||
uint16_t raw_value;
|
||||
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Read data failed");
|
||||
return false;
|
||||
}
|
||||
raw_value = i2c::i2ctohs(raw_value);
|
||||
|
||||
float lx = float(raw_value) / RESOLUTION_DIVISOR;
|
||||
lx *= float(MTREG_DEFAULT) / this->current_mtreg_;
|
||||
if (this->current_mode_ == BH1750_MODE_H2) {
|
||||
lx /= MODE_H2_DIVISOR;
|
||||
}
|
||||
|
||||
lx_out = lx;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BH1750Sensor::process_coarse_result_(float lx) {
|
||||
if (std::isnan(lx)) {
|
||||
// Use defaults if coarse measurement failed
|
||||
this->fine_mode_ = BH1750_MODE_H2;
|
||||
this->fine_mtreg_ = MTREG_MAX;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lx <= HIGH_LIGHT_THRESHOLD_LX) {
|
||||
this->fine_mode_ = BH1750_MODE_H2;
|
||||
this->fine_mtreg_ = MTREG_MAX;
|
||||
} else {
|
||||
this->fine_mode_ = BH1750_MODE_H;
|
||||
// lx = counts / 1.2 * (69 / mtreg)
|
||||
// -> mtreg = counts / 1.2 * (69 / lx)
|
||||
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
|
||||
// -> mtreg = 50000*(10/12)*(69/lx)
|
||||
int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx);
|
||||
this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg));
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_);
|
||||
}
|
||||
|
||||
void BH1750Sensor::fail_and_reset_() {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
this->state_ = IDLE;
|
||||
}
|
||||
|
||||
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace bh1750
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bh1750
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bh1750 {
|
||||
namespace esphome::bh1750 {
|
||||
|
||||
enum BH1750Mode {
|
||||
enum BH1750Mode : uint8_t {
|
||||
BH1750_MODE_L,
|
||||
BH1750_MODE_H,
|
||||
BH1750_MODE_H2,
|
||||
@@ -21,13 +20,36 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
|
||||
// State machine states
|
||||
enum State : uint8_t {
|
||||
IDLE,
|
||||
WAITING_COARSE_MEASUREMENT,
|
||||
READING_COARSE_RESULT,
|
||||
WAITING_FINE_MEASUREMENT,
|
||||
READING_FINE_RESULT,
|
||||
};
|
||||
|
||||
// 4-byte aligned members
|
||||
uint32_t measurement_start_time_{0};
|
||||
uint32_t measurement_duration_{0};
|
||||
|
||||
// 1-byte members grouped together to minimize padding
|
||||
State state_{IDLE};
|
||||
BH1750Mode current_mode_{BH1750_MODE_L};
|
||||
uint8_t current_mtreg_{31};
|
||||
BH1750Mode fine_mode_{BH1750_MODE_H2};
|
||||
uint8_t fine_mtreg_{254};
|
||||
uint8_t active_mtreg_{0};
|
||||
|
||||
// Helper methods
|
||||
bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now);
|
||||
bool read_measurement_(float &lx_out);
|
||||
void process_coarse_result_(float lx);
|
||||
void fail_and_reset_();
|
||||
};
|
||||
|
||||
} // namespace bh1750
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bh1750
|
||||
|
||||
@@ -122,19 +122,16 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
|
||||
bool result;
|
||||
std::vector<uint8_t> value;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: write directly from flash pointer
|
||||
result = this->write(this->value_.data, this->len_);
|
||||
// Static mode: copy from flash to vector
|
||||
value.assign(this->value_.data, this->value_.data + this->len_);
|
||||
} else {
|
||||
// Template mode: call function and write the vector
|
||||
std::vector<uint8_t> value = this->value_.func(x...);
|
||||
result = this->write(value);
|
||||
// Template mode: call function
|
||||
value = this->value_.func(x...);
|
||||
}
|
||||
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!result)
|
||||
if (!write(value))
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
@@ -147,15 +144,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
* errors.
|
||||
*/
|
||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||
bool write(const uint8_t *data, size_t len) {
|
||||
bool write(const std::vector<uint8_t> &value) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
||||
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
|
||||
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
|
||||
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
return false;
|
||||
@@ -163,8 +160,6 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
|
||||
@@ -96,10 +96,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -37,7 +37,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -389,8 +389,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (this->conn_id_ != param->search_res.conn_id)
|
||||
return false;
|
||||
this->service_count_++;
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// V3 clients don't need services initialized since
|
||||
// as they use the ESP APIs to get services.
|
||||
break;
|
||||
|
||||
@@ -383,7 +383,6 @@ async def to_code(config):
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
cg.add_define("USE_ETHERNET_MANUAL_IP")
|
||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||
|
||||
# Add compile-time define for PHY types with specific code
|
||||
|
||||
@@ -553,14 +553,11 @@ void EthernetComponent::start_connect_() {
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
info.ip = this->manual_ip_->static_ip;
|
||||
info.gw = this->manual_ip_->gateway;
|
||||
info.netmask = this->manual_ip_->subnet;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
} else {
|
||||
info.ip.addr = 0;
|
||||
info.gw.addr = 0;
|
||||
info.netmask.addr = 0;
|
||||
@@ -581,7 +578,6 @@ void EthernetComponent::start_connect_() {
|
||||
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
@@ -594,9 +590,7 @@ void EthernetComponent::start_connect_() {
|
||||
d = this->manual_ip_->dns2;
|
||||
dns_setserver(1, &d);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
} else {
|
||||
err = esp_netif_dhcpc_start(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
||||
@@ -694,9 +688,7 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
|
||||
@@ -82,9 +82,7 @@ class EthernetComponent : public Component {
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
@@ -139,9 +137,7 @@ class EthernetComponent : public Component {
|
||||
uint8_t mdc_pin_{23};
|
||||
uint8_t mdio_pin_{18};
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_{};
|
||||
#endif
|
||||
uint32_t connect_begin_;
|
||||
|
||||
// Group all uint8_t types together (enums and bools)
|
||||
|
||||
@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,6 @@ void LightState::setup() {
|
||||
effect->init_internal(this);
|
||||
}
|
||||
|
||||
// Start with loop disabled if idle - respects any effects/transitions set up during initialization
|
||||
this->disable_loop_if_idle_();
|
||||
|
||||
// When supported color temperature range is known, initialize color temperature setting within bounds.
|
||||
auto traits = this->get_traits();
|
||||
float min_mireds = traits.get_min_mireds();
|
||||
@@ -129,9 +126,6 @@ void LightState::loop() {
|
||||
this->is_transformer_active_ = false;
|
||||
this->transformer_ = nullptr;
|
||||
this->target_state_reached_callback_.call();
|
||||
|
||||
// Disable loop if idle (no transformer and no effect)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +133,6 @@ void LightState::loop() {
|
||||
if (this->next_write_) {
|
||||
this->next_write_ = false;
|
||||
this->output_->write_state(this);
|
||||
// Disable loop if idle (no transformer and no effect)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +228,6 @@ void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->active_effect_index_ = effect_index;
|
||||
auto *effect = this->get_active_effect_();
|
||||
effect->start_internal();
|
||||
// Enable loop while effect is active
|
||||
this->enable_loop();
|
||||
}
|
||||
LightEffect *LightState::get_active_effect_() {
|
||||
if (this->active_effect_index_ == 0) {
|
||||
@@ -252,8 +242,6 @@ void LightState::stop_effect_() {
|
||||
effect->stop();
|
||||
}
|
||||
this->active_effect_index_ = 0;
|
||||
// Disable loop if idle (no effect and no transformer)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
|
||||
void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
|
||||
@@ -263,8 +251,6 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng
|
||||
if (set_remote_values) {
|
||||
this->remote_values = target;
|
||||
}
|
||||
// Enable loop while transition is active
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
|
||||
@@ -280,8 +266,6 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
|
||||
if (set_remote_values) {
|
||||
this->remote_values = target;
|
||||
};
|
||||
// Enable loop while flash is active
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
|
||||
@@ -293,14 +277,6 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
|
||||
}
|
||||
this->output_->update_state(this);
|
||||
this->next_write_ = true;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::disable_loop_if_idle_() {
|
||||
// Only disable loop if both transformer and effect are inactive, and no pending writes
|
||||
if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void LightState::save_remote_values_() {
|
||||
|
||||
@@ -256,9 +256,6 @@ class LightState : public EntityBase, public Component {
|
||||
/// Internal method to save the current remote_values to the preferences
|
||||
void save_remote_values_();
|
||||
|
||||
/// Disable loop if neither transformer nor effect is active
|
||||
void disable_loop_if_idle_();
|
||||
|
||||
/// Store the output to allow effects to have more access.
|
||||
LightOutput *output_;
|
||||
/// The currently active transformer for this light (transition/flash).
|
||||
|
||||
@@ -365,10 +365,8 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
|
||||
|
||||
@@ -65,9 +65,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
uint16_t buffer_at = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
|
||||
MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
// Add newline if platform needs it (ESP32 doesn't add via write_msg_)
|
||||
this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer, buffer_at);
|
||||
this->write_msg_(console_buffer);
|
||||
}
|
||||
|
||||
// Reset the recursion guard for this task
|
||||
@@ -133,19 +131,18 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
|
||||
// Save the offset before calling format_log_to_buffer_with_terminator_
|
||||
// since it will increment tx_buffer_at_ to the end of the formatted string
|
||||
uint16_t msg_start = this->tx_buffer_at_;
|
||||
uint32_t msg_start = this->tx_buffer_at_;
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
|
||||
&this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
|
||||
uint16_t msg_length =
|
||||
// Write to console and send callback starting at the msg_start
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_ + msg_start);
|
||||
}
|
||||
size_t msg_length =
|
||||
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
|
||||
|
||||
// Callbacks get message first (before console write)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
|
||||
|
||||
// Write to console starting at the msg_start
|
||||
this->write_tx_buffer_to_console_(msg_start, &msg_length);
|
||||
|
||||
global_recursion_guard_ = false;
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -212,7 +209,9 @@ void Logger::process_messages_() {
|
||||
// This ensures all log messages appear on the console in a clean, serialized manner
|
||||
// Note: Messages may appear slightly out of order due to async processing, but
|
||||
// this is preferred over corrupted/interleaved console output
|
||||
this->write_tx_buffer_to_console_();
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
|
||||
@@ -71,17 +71,6 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Platform-specific: does write_msg_ add its own newline?
|
||||
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
|
||||
// Allows single write call with newline included for efficiency
|
||||
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
|
||||
// Newline should NOT be added to buffer
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
|
||||
#else
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -184,7 +173,7 @@ class Logger : public Component {
|
||||
|
||||
protected:
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
void write_msg_(const char *msg);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
@@ -211,35 +200,6 @@ class Logger : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to add newline to buffer for platforms that need it
|
||||
// Modifies buffer_at to include the newline
|
||||
inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
|
||||
if constexpr (!WRITE_MSG_ADDS_NEWLINE) {
|
||||
// Add newline - don't need to maintain null termination
|
||||
// write_msg_ now always receives explicit length, so we can safely overwrite the null terminator
|
||||
// This is safe because:
|
||||
// 1. Callbacks already received the message (before we add newline)
|
||||
// 2. write_msg_ receives the length explicitly (doesn't need null terminator)
|
||||
if (*buffer_at < buffer_size) {
|
||||
buffer[(*buffer_at)++] = '\n';
|
||||
} else if (buffer_size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
buffer[buffer_size - 1] = '\n';
|
||||
*buffer_at = buffer_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to write tx_buffer_ to console if logging is enabled
|
||||
// INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null
|
||||
inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) {
|
||||
if (this->baud_rate_ > 0) {
|
||||
uint16_t *len_ptr = length ? length : &this->tx_buffer_at_;
|
||||
this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset);
|
||||
this->write_msg_(this->tx_buffer_ + offset, *len_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format and send a log message to both console and callbacks
|
||||
inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args) {
|
||||
@@ -248,11 +208,10 @@ class Logger : public Component {
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
|
||||
// Callbacks get message WITHOUT newline (for API/MQTT/syslog)
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
|
||||
}
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
|
||||
|
||||
// Console gets message WITH newline (if platform needs it)
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
|
||||
// Write the body of the log message to the buffer
|
||||
@@ -466,9 +425,7 @@ class Logger : public Component {
|
||||
}
|
||||
|
||||
// Update buffer_at with the formatted length (handle truncation)
|
||||
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
|
||||
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
|
||||
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
|
||||
uint16_t formatted_len = (ret >= remaining) ? remaining : ret;
|
||||
*buffer_at += formatted_len;
|
||||
|
||||
// Remove all trailing newlines right after formatting
|
||||
|
||||
@@ -121,23 +121,25 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Length is now always passed explicitly - no strlen() fallback needed
|
||||
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
// USB CDC/JTAG - single write including newline (already in buffer)
|
||||
// Use fwrite to stdout which goes through VFS to USB console
|
||||
//
|
||||
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
|
||||
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
|
||||
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
|
||||
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
|
||||
// to configure correctly for their hardware. This approach eliminates runtime overhead.
|
||||
fwrite(msg, 1, len, stdout);
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
if (
|
||||
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC
|
||||
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
|
||||
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#else
|
||||
// Regular UART - single write including newline (already in buffer)
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
/* DISABLES CODE */ (false) // NOLINT
|
||||
#endif
|
||||
) {
|
||||
puts(msg);
|
||||
} else {
|
||||
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
|
||||
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
uart_write_bytes(this->uart_num_, "\n", 1);
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
|
||||
@@ -33,10 +33,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
this->hw_serial_->write(msg, len);
|
||||
}
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
@@ -49,7 +49,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); }
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -27,7 +27,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -62,7 +62,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
|
||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_reg_(pin, false, iodir);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number.automation";
|
||||
|
||||
@@ -51,4 +52,5 @@ void ValueRangeTrigger::on_state_(float state) {
|
||||
this->rtc_.save(&in_range);
|
||||
}
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class NumberStateTrigger : public Trigger<float> {
|
||||
public:
|
||||
@@ -90,4 +91,5 @@ template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...>
|
||||
float max_{NAN};
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
@@ -42,4 +43,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include "number_call.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class Number;
|
||||
void log_number(const char *tag, const char *prefix, const char *type, Number *obj);
|
||||
@@ -52,4 +53,5 @@ class Number : public EntityBase {
|
||||
CallbackManager<void(float)> state_callback_;
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
#include "number.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
@@ -124,4 +125,5 @@ void NumberCall::perform() {
|
||||
this->parent_->control(target_value);
|
||||
}
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class Number;
|
||||
|
||||
@@ -43,4 +44,5 @@ class NumberCall {
|
||||
bool cycle_;
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_AUTO = 0,
|
||||
@@ -34,4 +35,5 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas
|
||||
NumberMode mode_{NUMBER_MODE_AUTO};
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -174,9 +174,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_OPENTHREAD")
|
||||
|
||||
# OpenThread uses esp_vfs_eventfd which requires VFS select support
|
||||
require_vfs_select()
|
||||
|
||||
# OpenThread SRP needs access to mDNS services after setup
|
||||
enable_mdns_storage()
|
||||
|
||||
|
||||
@@ -46,14 +46,14 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
|
||||
|
||||
// execute this script using a tuple that contains the arguments
|
||||
void execute_tuple(const std::tuple<Ts...> &tuple) {
|
||||
this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
// Internal function to give scripts readable names.
|
||||
void set_name(const LogString *name) { name_ = name; }
|
||||
|
||||
protected:
|
||||
template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->execute(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
||||
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
|
||||
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
|
||||
this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
}
|
||||
}
|
||||
|
||||
template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->trigger(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
|
||||
while (!this->param_queue_.empty()) {
|
||||
auto ¶ms = this->param_queue_.front();
|
||||
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
|
||||
this->param_queue_.pop_front();
|
||||
}
|
||||
// Queue is now empty - disable loop until next play_complex
|
||||
@@ -321,7 +321,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,21 +77,23 @@ class Select : public EntityBase {
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control().
|
||||
* Override this to work directly with indices and avoid string conversions.
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
protected:
|
||||
friend class SelectCall;
|
||||
|
||||
size_t active_index_{0};
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
* Overriding this index-based version is PREFERRED as it avoids string conversions.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control(const std::string&).
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
/** Set the value of the select, this is a virtual method that each select integration can implement.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
|
||||
@@ -74,9 +74,9 @@ StateClass Sensor::get_state_class() {
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
this->callbacks_.call_first(this->raw_count_, state);
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
|
||||
|
||||
@@ -87,12 +87,12 @@ void Sensor::publish_state(float state) {
|
||||
}
|
||||
}
|
||||
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->callbacks_.add_second(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
|
||||
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
|
||||
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
|
||||
if (!this->raw_callback_) {
|
||||
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
|
||||
}
|
||||
this->raw_callback_->add(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::add_filter(Filter *filter) {
|
||||
@@ -132,10 +132,7 @@ void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->state = state;
|
||||
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
|
||||
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
|
||||
|
||||
// Call filtered callbacks (after filters)
|
||||
this->callbacks_.call_second(this->raw_count_, state);
|
||||
|
||||
this->callback_.call(state);
|
||||
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
|
||||
@@ -124,7 +124,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
|
||||
void internal_send_state_to_frontend(float state);
|
||||
|
||||
protected:
|
||||
PartitionedCallbackManager<void(float)> callbacks_;
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
|
||||
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
@@ -139,8 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
|
||||
uint8_t force_update : 1;
|
||||
uint8_t reserved : 5; // Reserved for future use
|
||||
} sensor_flags_{};
|
||||
|
||||
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
|
||||
};
|
||||
|
||||
} // namespace sensor
|
||||
|
||||
@@ -137,11 +137,7 @@ async def to_code(config):
|
||||
cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME]))
|
||||
supports_arm_night = True
|
||||
|
||||
if sensors := config.get(CONF_BINARY_SENSORS, []):
|
||||
# Initialize FixedVector with the exact number of sensors
|
||||
cg.add(var.init_sensors(len(sensors)))
|
||||
|
||||
for sensor in sensors:
|
||||
for sensor in config.get(CONF_BINARY_SENSORS, []):
|
||||
bs = await cg.get_variable(sensor[CONF_INPUT])
|
||||
|
||||
flags = BinarySensorFlags[FLAG_NORMAL]
|
||||
|
||||
@@ -20,13 +20,10 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
||||
// Save the flags and type. Assign a store index for the per sensor data type.
|
||||
SensorDataStore sd;
|
||||
sd.last_chime_state = false;
|
||||
AlarmSensor alarm_sensor;
|
||||
alarm_sensor.sensor = sensor;
|
||||
alarm_sensor.info.flags = flags;
|
||||
alarm_sensor.info.type = type;
|
||||
alarm_sensor.info.store_index = this->next_store_index_++;
|
||||
this->sensors_.push_back(alarm_sensor);
|
||||
this->sensor_map_[sensor].flags = flags;
|
||||
this->sensor_map_[sensor].type = type;
|
||||
this->sensor_data_.push_back(sd);
|
||||
this->sensor_map_[sensor].store_index = this->next_store_index_++;
|
||||
};
|
||||
|
||||
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
@@ -48,7 +45,7 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"TemplateAlarmControlPanel:\n"
|
||||
" Current State: %s\n"
|
||||
" Number of Codes: %zu\n"
|
||||
" Number of Codes: %u\n"
|
||||
" Requires Code To Arm: %s\n"
|
||||
" Arming Away Time: %" PRIu32 "s\n"
|
||||
" Arming Home Time: %" PRIu32 "s\n"
|
||||
@@ -61,8 +58,7 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
(this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000),
|
||||
(this->trigger_time_ / 1000), this->get_supported_features());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &alarm_sensor : this->sensors_) {
|
||||
const uint16_t flags = alarm_sensor.info.flags;
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Binary Sensor:\n"
|
||||
" Name: %s\n"
|
||||
@@ -71,10 +67,11 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
" Armed night bypass: %s\n"
|
||||
" Auto bypass: %s\n"
|
||||
" Chime mode: %s",
|
||||
alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)),
|
||||
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
|
||||
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
|
||||
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME));
|
||||
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -124,9 +121,7 @@ void TemplateAlarmControlPanel::loop() {
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Test all of the sensors regardless of the alarm panel state
|
||||
for (const auto &alarm_sensor : this->sensors_) {
|
||||
const auto &info = alarm_sensor.info;
|
||||
auto *sensor = alarm_sensor.sensor;
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
// Check for chime zones
|
||||
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
|
||||
// Look for the transition from closed to open
|
||||
@@ -247,11 +242,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
|
||||
|
||||
void TemplateAlarmControlPanel::bypass_before_arming() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &alarm_sensor : this->sensors_) {
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
// Check for faulted bypass_auto sensors and remove them from monitoring
|
||||
if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) {
|
||||
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str());
|
||||
this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index);
|
||||
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
|
||||
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
|
||||
this->bypassed_sensor_indicies_.push_back(info.store_index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
|
||||
|
||||
@@ -50,13 +49,6 @@ struct SensorInfo {
|
||||
uint8_t store_index;
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct AlarmSensor {
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
SensorInfo info;
|
||||
};
|
||||
#endif
|
||||
|
||||
class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component {
|
||||
public:
|
||||
TemplateAlarmControlPanel();
|
||||
@@ -71,12 +63,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
void bypass_before_arming();
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
/** Initialize the sensors vector with the specified capacity.
|
||||
*
|
||||
* @param capacity The number of sensors to allocate space for.
|
||||
*/
|
||||
void init_sensors(size_t capacity) { this->sensors_.init(capacity); }
|
||||
|
||||
/** Add a binary_sensor to the alarm_panel.
|
||||
*
|
||||
* @param sensor The BinarySensor instance.
|
||||
@@ -136,8 +122,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
protected:
|
||||
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// List of binary sensors with their alarm-specific info
|
||||
FixedVector<AlarmSensor> sensors_;
|
||||
// This maps a binary sensor to its alarm specific info
|
||||
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
|
||||
// a list of automatically bypassed sensors
|
||||
std::vector<uint8_t> bypassed_sensor_indicies_;
|
||||
#endif
|
||||
|
||||
@@ -26,9 +26,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
|
||||
|
||||
void TextSensor::publish_state(const std::string &state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
this->callbacks_.call_first(this->raw_count_, state);
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
|
||||
|
||||
@@ -70,11 +70,13 @@ void TextSensor::clear_filters() {
|
||||
}
|
||||
|
||||
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
|
||||
this->callbacks_.add_second(std::move(callback));
|
||||
this->callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
|
||||
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
|
||||
if (!this->raw_callback_) {
|
||||
this->raw_callback_ = make_unique<CallbackManager<void(std::string)>>();
|
||||
}
|
||||
this->raw_callback_->add(std::move(callback));
|
||||
}
|
||||
|
||||
std::string TextSensor::get_state() const { return this->state; }
|
||||
@@ -83,10 +85,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
|
||||
this->state = state;
|
||||
this->set_has_state(true);
|
||||
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
|
||||
|
||||
// Call filtered callbacks (after filters)
|
||||
this->callbacks_.call_second(this->raw_count_, state);
|
||||
|
||||
this->callback_.call(state);
|
||||
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_text_sensor_update(this);
|
||||
#endif
|
||||
|
||||
@@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
void internal_send_state_to_frontend(const std::string &state);
|
||||
|
||||
protected:
|
||||
PartitionedCallbackManager<void(std::string)> callbacks_;
|
||||
std::unique_ptr<CallbackManager<void(std::string)>>
|
||||
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
|
||||
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
|
||||
};
|
||||
|
||||
} // namespace text_sensor
|
||||
|
||||
@@ -353,9 +353,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
void AsyncResponseStream::print(float value) {
|
||||
// Use stack buffer to avoid temporary string allocation
|
||||
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
|
||||
constexpr size_t float_buf_size = 32;
|
||||
char buf[float_buf_size];
|
||||
int len = snprintf(buf, float_buf_size, "%f", value);
|
||||
char buf[32];
|
||||
int len = snprintf(buf, sizeof(buf), "%f", value);
|
||||
this->content_.append(buf, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -1414,7 +1414,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
|
||||
this->set_sta_priority(failed_bssid.value(), new_priority);
|
||||
}
|
||||
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s,
|
||||
|
||||
@@ -710,15 +710,6 @@ class EsphomeCore:
|
||||
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
|
||||
return self.relative_build_path(".piolibdeps", *path)
|
||||
|
||||
@property
|
||||
def platformio_cache_dir(self) -> str:
|
||||
"""Get the PlatformIO cache directory path."""
|
||||
# Check if running in Docker/HA addon with custom cache dir
|
||||
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
|
||||
return cache_dir
|
||||
# Default PlatformIO cache location
|
||||
return os.path.expanduser("~/.platformio/.cache")
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
if self.is_libretiny:
|
||||
|
||||
@@ -11,26 +11,10 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// C++20 std::index_sequence is now used for tuple unpacking
|
||||
// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility
|
||||
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
|
||||
// Remove before 2026.6.0
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
|
||||
template<int...> struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {};
|
||||
template<int N, int... S>
|
||||
struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens
|
||||
: gens<N - 1, N - 1, S...> {};
|
||||
template<int... S> struct gens<0, S...> { using type = seq<S...>; };
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
template<int...> struct seq {}; // NOLINT
|
||||
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
|
||||
template<int... S> struct gens<0, S...> { using type = seq<S...>; }; // NOLINT
|
||||
|
||||
#define TEMPLATABLE_VALUE_(type, name) \
|
||||
protected: \
|
||||
@@ -168,11 +152,11 @@ template<typename... Ts> class Condition {
|
||||
|
||||
/// Call check with a tuple of values as parameter.
|
||||
bool check_tuple(const std::tuple<Ts...> &tuple) {
|
||||
return this->check_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return this->check_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> bool check_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> bool check_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
return this->check(std::get<S>(tuple)...);
|
||||
}
|
||||
};
|
||||
@@ -247,11 +231,11 @@ template<typename... Ts> class Action {
|
||||
}
|
||||
}
|
||||
}
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
void play_next_tuple_(const std::tuple<Ts...> &tuple) {
|
||||
this->play_next_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->play_next_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
virtual void stop() {}
|
||||
@@ -293,9 +277,7 @@ template<typename... Ts> class ActionList {
|
||||
if (this->actions_begin_ != nullptr)
|
||||
this->actions_begin_->play_complex(x...);
|
||||
}
|
||||
void play_tuple(const std::tuple<Ts...> &tuple) {
|
||||
this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
}
|
||||
void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, typename gens<sizeof...(Ts)>::type()); }
|
||||
void stop() {
|
||||
if (this->actions_begin_ != nullptr)
|
||||
this->actions_begin_->stop_complex();
|
||||
@@ -316,7 +298,7 @@ template<typename... Ts> class ActionList {
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
TEMPLATABLE_VALUE(uint32_t, delay)
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||
this->num_running_++;
|
||||
|
||||
// If num_running_ > 1, we have multiple instances running in parallel
|
||||
@@ -186,22 +187,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
// WARNING: This can accumulate delays if scripts are triggered faster than they complete!
|
||||
// Users should set max_runs on parallel scripts to limit concurrent executions.
|
||||
// Issue #10264: This is a workaround for parallel script delays interfering with each other.
|
||||
|
||||
// Optimization: For no-argument delays (most common case), use direct lambda
|
||||
// instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution)
|
||||
if constexpr (sizeof...(Ts) == 0) {
|
||||
App.scheduler.set_timer_common_(
|
||||
this, Scheduler::SchedulerItem::TIMEOUT,
|
||||
/* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); },
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
} else {
|
||||
// For delays with arguments, use std::bind to preserve argument values
|
||||
// Arguments must be copied because original references may be invalid after delay
|
||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT,
|
||||
/* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f),
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
}
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT,
|
||||
/* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f),
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
|
||||
@@ -216,7 +216,6 @@
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||
#define USE_ETHERNET
|
||||
#define USE_ETHERNET_KSZ8081
|
||||
#define USE_ETHERNET_MANUAL_IP
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
@@ -74,12 +74,6 @@ void EntityBase::set_object_id(const char *object_id) {
|
||||
this->calc_object_id_();
|
||||
}
|
||||
|
||||
void EntityBase::set_name_and_object_id(const char *name, const char *object_id) {
|
||||
this->set_name(name);
|
||||
this->object_id_c_str_ = object_id;
|
||||
this->calc_object_id_();
|
||||
}
|
||||
|
||||
// Calculate Object ID Hash from Entity Name
|
||||
void EntityBase::calc_object_id_() {
|
||||
this->object_id_hash_ =
|
||||
|
||||
@@ -41,9 +41,6 @@ class EntityBase {
|
||||
std::string get_object_id() const;
|
||||
void set_object_id(const char *object_id);
|
||||
|
||||
// Set both name and object_id in one call (reduces generated code size)
|
||||
void set_name_and_object_id(const char *name, const char *object_id);
|
||||
|
||||
// Get the unique Object ID of this Entity
|
||||
uint32_t get_object_id_hash();
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
# Get device name for object ID calculation
|
||||
device_name = device_id_obj.id
|
||||
|
||||
add(var.set_name(config[CONF_NAME]))
|
||||
|
||||
# Calculate base object_id using the same logic as C++
|
||||
# This must match the C++ behavior in esphome/core/entity_base.cpp
|
||||
base_object_id = get_base_entity_object_id(
|
||||
@@ -95,8 +97,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"Entity has empty name, using '%s' as object_id base", base_object_id
|
||||
)
|
||||
|
||||
# Set both name and object_id in one call to reduce generated code size
|
||||
add(var.set_name_and_object_id(config[CONF_NAME], base_object_id))
|
||||
# Set the object ID
|
||||
add(var.set_object_id(base_object_id))
|
||||
_LOGGER.debug(
|
||||
"Setting object_id '%s' for entity '%s' on platform '%s'",
|
||||
base_object_id,
|
||||
|
||||
@@ -414,8 +414,10 @@ int8_t step_to_accuracy_decimals(float step) {
|
||||
return str.length() - dot_pos - 1;
|
||||
}
|
||||
|
||||
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
|
||||
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Helper function to find the index of a base64 character in the lookup table.
|
||||
// Returns the character's position (0-63) if found, or 0 if not found.
|
||||
@@ -425,8 +427,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
|
||||
// stops processing at the first invalid character due to the is_base64() check in its
|
||||
// while loop condition, making this edge case harmless in practice.
|
||||
static inline uint8_t base64_find_char(char c) {
|
||||
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
|
||||
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
|
||||
const char *pos = strchr(BASE64_CHARS, c);
|
||||
return pos ? (pos - BASE64_CHARS) : 0;
|
||||
}
|
||||
|
||||
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
|
||||
|
||||
@@ -162,9 +162,6 @@ template<typename T, size_t N> class StaticVector {
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
|
||||
// Direct access to size counter for efficient in-place construction
|
||||
size_t &count() { return count_; }
|
||||
|
||||
T &operator[](size_t i) { return data_[i]; }
|
||||
const T &operator[](size_t i) const { return data_[i]; }
|
||||
|
||||
@@ -889,73 +886,6 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
std::vector<std::function<void(Ts...)>> callbacks_;
|
||||
};
|
||||
|
||||
template<typename... X> class PartitionedCallbackManager;
|
||||
|
||||
/** Helper class for callbacks partitioned into two sections.
|
||||
*
|
||||
* Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1]
|
||||
* The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment).
|
||||
*
|
||||
* Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms).
|
||||
* The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste.
|
||||
*
|
||||
* Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value)
|
||||
* is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count
|
||||
* internally, saving memory per instance.
|
||||
*
|
||||
* @tparam Ts The arguments for the callbacks, wrapped in void().
|
||||
*/
|
||||
template<typename... Ts> class PartitionedCallbackManager<void(Ts...)> {
|
||||
public:
|
||||
/// Add a callback to the first partition.
|
||||
void add_first(std::function<void(Ts...)> &&callback, uint8_t *first_count) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
|
||||
}
|
||||
|
||||
// Add to first partition: append then rotate into position
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
// Avoid potential underflow: rewrite comparison to not subtract from size()
|
||||
if (*first_count + 1 < this->callbacks_->size()) {
|
||||
// Use std::rotate to maintain registration order in second partition
|
||||
std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end());
|
||||
}
|
||||
(*first_count)++;
|
||||
}
|
||||
|
||||
/// Add a callback to the second partition.
|
||||
void add_second(std::function<void(Ts...)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
|
||||
}
|
||||
|
||||
// Add to second partition: just append (already at end after first partition)
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
/// Call all callbacks in the first partition.
|
||||
void call_first(uint8_t first_count, Ts... args) {
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < first_count; i++) {
|
||||
(*this->callbacks_)[i](args...);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call all callbacks in the second partition.
|
||||
void call_second(uint8_t first_count, Ts... args) {
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = first_count; i < this->callbacks_->size(); i++) {
|
||||
(*this->callbacks_)[i](args...);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1]
|
||||
std::unique_ptr<std::vector<std::function<void(Ts...)>>> callbacks_;
|
||||
};
|
||||
|
||||
/// Helper class to deduplicate items in a series of values.
|
||||
template<typename T> class Deduplicator {
|
||||
public:
|
||||
|
||||
@@ -19,21 +19,11 @@ from esphome.core import (
|
||||
TimePeriodNanoseconds,
|
||||
TimePeriodSeconds,
|
||||
)
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
|
||||
from esphome.types import Expression, SafeExpType, TemplateArgsType
|
||||
from esphome.util import OrderedDict
|
||||
from esphome.yaml_util import ESPHomeDataBase
|
||||
|
||||
# Keys for lambda deduplication storage in CORE.data
|
||||
_KEY_LAMBDA_DEDUP = "lambda_dedup"
|
||||
_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations"
|
||||
|
||||
# Regex patterns for static variable detection (compiled once)
|
||||
_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE)
|
||||
_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+")
|
||||
|
||||
|
||||
class RawExpression(Expression):
|
||||
__slots__ = ("text",)
|
||||
@@ -198,7 +188,7 @@ class LambdaExpression(Expression):
|
||||
|
||||
def __init__(
|
||||
self, parts, parameters, capture: str = "=", return_type=None, source=None
|
||||
) -> None:
|
||||
):
|
||||
self.parts = parts
|
||||
if not isinstance(parameters, ParameterListExpression):
|
||||
parameters = ParameterListExpression(*parameters)
|
||||
@@ -207,21 +197,16 @@ class LambdaExpression(Expression):
|
||||
self.capture = capture
|
||||
self.return_type = safe_exp(return_type) if return_type is not None else None
|
||||
|
||||
def format_body(self) -> str:
|
||||
"""Format the lambda body with source directive and content."""
|
||||
body = ""
|
||||
if self.source is not None:
|
||||
body += f"{self.source.as_line_directive}\n"
|
||||
body += self.content
|
||||
return body
|
||||
|
||||
def __str__(self) -> str:
|
||||
def __str__(self):
|
||||
# Stateless lambdas (empty capture) implicitly convert to function pointers
|
||||
# when assigned to function pointer types - no unary + needed
|
||||
cpp = f"[{self.capture}]({self.parameters})"
|
||||
if self.return_type is not None:
|
||||
cpp += f" -> {self.return_type}"
|
||||
cpp += f" {{\n{self.format_body()}\n}}"
|
||||
cpp += " {\n"
|
||||
if self.source is not None:
|
||||
cpp += f"{self.source.as_line_directive}\n"
|
||||
cpp += f"{self.content}\n}}"
|
||||
return indent_all_but_first_and_last(cpp)
|
||||
|
||||
@property
|
||||
@@ -229,37 +214,6 @@ class LambdaExpression(Expression):
|
||||
return "".join(str(part) for part in self.parts)
|
||||
|
||||
|
||||
class SharedFunctionLambdaExpression(LambdaExpression):
|
||||
"""A lambda expression that references a shared deduplicated function.
|
||||
|
||||
This class wraps a function pointer but maintains the LambdaExpression
|
||||
interface so calling code works unchanged.
|
||||
"""
|
||||
|
||||
__slots__ = ("_func_name",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func_name: str,
|
||||
parameters: TemplateArgsType,
|
||||
return_type: SafeExpType | None = None,
|
||||
) -> None:
|
||||
# Initialize parent with empty parts since we're just a function reference
|
||||
super().__init__(
|
||||
[], parameters, capture="", return_type=return_type, source=None
|
||||
)
|
||||
self._func_name = func_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
# Just return the function name - it's already a function pointer
|
||||
return self._func_name
|
||||
|
||||
@property
|
||||
def content(self) -> str:
|
||||
# No content, just a function reference
|
||||
return ""
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class Literal(Expression, metaclass=abc.ABCMeta):
|
||||
__slots__ = ()
|
||||
@@ -629,25 +583,6 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
|
||||
CORE.add_global(expression, prepend)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def flush_lambda_dedup_declarations() -> None:
|
||||
"""Flush all deferred lambda deduplication declarations to global scope.
|
||||
|
||||
This is a coroutine that runs with FINAL priority (after all components)
|
||||
to ensure all referenced variables are declared before the shared
|
||||
lambda functions that use them.
|
||||
"""
|
||||
if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data:
|
||||
return
|
||||
|
||||
declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS]
|
||||
for func_declaration in declarations:
|
||||
add_global(RawStatement(func_declaration))
|
||||
|
||||
# Clear the list so we don't add them again
|
||||
CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = []
|
||||
|
||||
|
||||
def add_library(name: str, version: str | None, repository: str | None = None):
|
||||
"""Add a library to the codegen library storage.
|
||||
|
||||
@@ -721,93 +656,6 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
|
||||
return await CORE.get_variable_with_full_id(id_)
|
||||
|
||||
|
||||
def _has_static_variables(code: str) -> bool:
|
||||
"""Check if code contains static variable definitions.
|
||||
|
||||
Static variables in lambdas should not be deduplicated because each lambda
|
||||
instance should have its own static variable state.
|
||||
|
||||
Args:
|
||||
code: The lambda body code to check
|
||||
|
||||
Returns:
|
||||
True if code contains static variable definitions
|
||||
"""
|
||||
# Remove C++ comments to avoid false positives
|
||||
# Remove single-line comments (// ...)
|
||||
code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code)
|
||||
# Remove multi-line comments (/* ... */)
|
||||
code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments)
|
||||
|
||||
# Match: static <type> <identifier>
|
||||
# But not: static_cast, static_assert, static_pointer_cast
|
||||
return bool(_RE_STATIC_VARIABLE.search(code_no_comments))
|
||||
|
||||
|
||||
def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None:
|
||||
"""Get the shared function name for a lambda expression.
|
||||
|
||||
If an identical lambda was already generated, returns the existing shared
|
||||
function name. Otherwise, creates a new shared function and returns its name.
|
||||
|
||||
Lambdas with static variables are not deduplicated to preserve their
|
||||
independent state.
|
||||
|
||||
Args:
|
||||
lambda_expr: The lambda expression to deduplicate
|
||||
|
||||
Returns:
|
||||
The name of the shared function for this lambda (either existing or newly created),
|
||||
or None if the lambda should not be deduplicated (e.g., contains static variables)
|
||||
"""
|
||||
# Create a unique key from the lambda content, parameters, and return type
|
||||
content = lambda_expr.content
|
||||
|
||||
# Don't deduplicate lambdas with static variables - each instance needs its own state
|
||||
if _has_static_variables(content):
|
||||
return None
|
||||
param_str = str(lambda_expr.parameters)
|
||||
return_str = (
|
||||
str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void"
|
||||
)
|
||||
|
||||
# Use tuple of (content, params, return_type) as key
|
||||
lambda_key = (content, param_str, return_str)
|
||||
|
||||
# Initialize deduplication storage in CORE.data if not exists
|
||||
if _KEY_LAMBDA_DEDUP not in CORE.data:
|
||||
CORE.data[_KEY_LAMBDA_DEDUP] = {}
|
||||
# Register the flush job to run after all components (FINAL priority)
|
||||
# This ensures all variables are declared before shared lambda functions
|
||||
CORE.add_job(flush_lambda_dedup_declarations)
|
||||
|
||||
lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP]
|
||||
|
||||
# Check if we've seen this lambda before
|
||||
if lambda_key in lambda_cache:
|
||||
# Return name of existing shared function
|
||||
return lambda_cache[lambda_key]
|
||||
|
||||
# First occurrence - create a shared function
|
||||
# Use the cache size as the function number
|
||||
func_name = f"shared_lambda_{len(lambda_cache)}"
|
||||
|
||||
# Build the function declaration using lambda's body formatting
|
||||
func_declaration = (
|
||||
f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}"
|
||||
)
|
||||
|
||||
# Store the declaration to be added later (after all variable declarations)
|
||||
# We can't add it immediately because it might reference variables not yet declared
|
||||
CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration)
|
||||
|
||||
# Store in cache
|
||||
lambda_cache[lambda_key] = func_name
|
||||
|
||||
# Return the function name (this is the first occurrence, but we still generate shared function)
|
||||
return func_name
|
||||
|
||||
|
||||
async def process_lambda(
|
||||
value: Lambda,
|
||||
parameters: TemplateArgsType,
|
||||
@@ -865,19 +713,6 @@ async def process_lambda(
|
||||
location.line += value.content_offset
|
||||
else:
|
||||
location = None
|
||||
|
||||
# Lambda deduplication: Only deduplicate stateless lambdas (empty capture).
|
||||
# Stateful lambdas cannot be shared as they capture different contexts.
|
||||
# Lambdas with static variables are also not deduplicated to preserve independent state.
|
||||
if capture == "":
|
||||
lambda_expr = LambdaExpression(
|
||||
parts, parameters, capture, return_type, location
|
||||
)
|
||||
func_name = _get_shared_lambda_name(lambda_expr)
|
||||
if func_name is not None:
|
||||
# Return a shared function reference instead of inline lambda
|
||||
return SharedFunctionLambdaExpression(func_name, parameters, return_type)
|
||||
|
||||
return LambdaExpression(parts, parameters, capture, return_type, location)
|
||||
|
||||
|
||||
|
||||
@@ -145,16 +145,7 @@ def run_compile(config, verbose):
|
||||
args = []
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
|
||||
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
|
||||
result = run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
# Run memory analysis if enabled
|
||||
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
|
||||
try:
|
||||
analyze_memory_usage(config)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("Failed to analyze memory usage: %s", e)
|
||||
|
||||
return result
|
||||
return run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
|
||||
def _run_idedata(config):
|
||||
@@ -403,73 +394,3 @@ class IDEData:
|
||||
if path.endswith(".exe")
|
||||
else f"{path[:-3]}readelf"
|
||||
)
|
||||
|
||||
|
||||
def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
"""Analyze memory usage by component after compilation."""
|
||||
# Lazy import to avoid overhead when not needed
|
||||
from esphome.analyze_memory import MemoryAnalyzer
|
||||
|
||||
idedata = get_idedata(config)
|
||||
|
||||
# Get paths to tools
|
||||
elf_path = idedata.firmware_elf_path
|
||||
objdump_path = idedata.objdump_path
|
||||
readelf_path = idedata.readelf_path
|
||||
|
||||
# Debug logging
|
||||
_LOGGER.debug("ELF path from idedata: %s", elf_path)
|
||||
|
||||
# Check if file exists
|
||||
if not Path(elf_path).exists():
|
||||
# Try alternate path
|
||||
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
|
||||
if alt_path.exists():
|
||||
elf_path = str(alt_path)
|
||||
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
|
||||
else:
|
||||
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
|
||||
return
|
||||
|
||||
# Extract external components from config
|
||||
external_components = set()
|
||||
|
||||
# Get the list of built-in ESPHome components
|
||||
from esphome.analyze_memory import get_esphome_components
|
||||
|
||||
builtin_components = get_esphome_components()
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
NON_COMPONENT_KEYS = {
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"<<",
|
||||
}
|
||||
|
||||
# Check all top-level keys in config
|
||||
for key in config:
|
||||
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
|
||||
# This is an external component
|
||||
external_components.add(key)
|
||||
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Create analyzer and run analysis
|
||||
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and print report
|
||||
report = analyzer.generate_report()
|
||||
_LOGGER.info("\n%s", report)
|
||||
|
||||
# Optionally save to file
|
||||
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
|
||||
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
|
||||
if report_file.suffix == ".json":
|
||||
report_file.write_text(analyzer.to_json())
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
else:
|
||||
report_file.write_text(report)
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Tests for the text component."""
|
||||
|
||||
from esphome.core import CORE
|
||||
"""Tests for the binary sensor component."""
|
||||
|
||||
|
||||
def test_text_is_setup(generate_main):
|
||||
@@ -58,22 +56,15 @@ def test_text_config_value_mode_set(generate_main):
|
||||
assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp
|
||||
|
||||
|
||||
def test_text_config_lambda_is_set(generate_main) -> None:
|
||||
def test_text_config_lamda_is_set(generate_main):
|
||||
"""
|
||||
Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication)
|
||||
Test if lambda is set for lambda mode (optimized with stateless lambda)
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Get both global and main sections to find the shared lambda definition
|
||||
full_cpp = CORE.cpp_global_section + main_cpp
|
||||
|
||||
# Then
|
||||
# Lambda is deduplicated into a shared function (reference in main section)
|
||||
assert "it_4->set_template(shared_lambda_" in main_cpp
|
||||
# Lambda body should be in the code somewhere
|
||||
assert 'return std::string{"Hello"};' in full_cpp
|
||||
# Verify the shared lambda function is defined (in global section)
|
||||
assert "esphome::optional<std::string> shared_lambda_" in full_cpp
|
||||
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
|
||||
assert 'return std::string{"Hello"};' in main_cpp
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
esphome:
|
||||
name: template-alarm-many-sensors
|
||||
friendly_name: "Template Alarm Control Panel with Many Sensors"
|
||||
|
||||
logger:
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: sensor1
|
||||
name: "Door 1"
|
||||
- platform: template
|
||||
id: sensor2
|
||||
name: "Door 2"
|
||||
- platform: template
|
||||
id: sensor3
|
||||
name: "Window 1"
|
||||
- platform: template
|
||||
id: sensor4
|
||||
name: "Window 2"
|
||||
- platform: template
|
||||
id: sensor5
|
||||
name: "Motion 1"
|
||||
- platform: template
|
||||
id: sensor6
|
||||
name: "Motion 2"
|
||||
- platform: template
|
||||
id: sensor7
|
||||
name: "Glass Break 1"
|
||||
- platform: template
|
||||
id: sensor8
|
||||
name: "Glass Break 2"
|
||||
- platform: template
|
||||
id: sensor9
|
||||
name: "Smoke Detector"
|
||||
- platform: template
|
||||
id: sensor10
|
||||
name: "CO Detector"
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
id: test_alarm
|
||||
name: "Test Alarm"
|
||||
codes:
|
||||
- "1234"
|
||||
requires_code_to_arm: true
|
||||
arming_away_time: 5s
|
||||
arming_home_time: 3s
|
||||
arming_night_time: 3s
|
||||
pending_time: 10s
|
||||
trigger_time: 300s
|
||||
restore_mode: ALWAYS_DISARMED
|
||||
binary_sensors:
|
||||
- input: sensor1
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor2
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor3
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor4
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor5
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: true
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor6
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: true
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor7
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor8
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor9
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT_ALWAYS
|
||||
- input: sensor10
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT_ALWAYS
|
||||
on_disarmed:
|
||||
- logger.log: "Alarm disarmed"
|
||||
on_arming:
|
||||
- logger.log: "Alarm arming"
|
||||
on_armed_away:
|
||||
- logger.log: "Alarm armed away"
|
||||
on_armed_home:
|
||||
- logger.log: "Alarm armed home"
|
||||
on_armed_night:
|
||||
- logger.log: "Alarm armed night"
|
||||
on_pending:
|
||||
- logger.log: "Alarm pending"
|
||||
on_triggered:
|
||||
- logger.log: "Alarm triggered"
|
||||
on_cleared:
|
||||
- logger.log: "Alarm cleared"
|
||||
on_chime:
|
||||
- logger.log: "Chime activated"
|
||||
on_ready:
|
||||
- logger.log: "Sensors ready state changed"
|
||||
@@ -1,118 +0,0 @@
|
||||
"""Integration test for template alarm control panel with many sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi.model import APIIntEnum
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
class EspHomeACPFeatures(APIIntEnum):
|
||||
"""ESPHome AlarmControlPanel feature numbers."""
|
||||
|
||||
ARM_HOME = 1
|
||||
ARM_AWAY = 2
|
||||
ARM_NIGHT = 4
|
||||
TRIGGER = 8
|
||||
ARM_CUSTOM_BYPASS = 16
|
||||
ARM_VACATION = 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_template_alarm_control_panel_many_sensors(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test template alarm control panel with 10 binary sensors using FixedVector."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Get entity info first
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the alarm control panel and binary sensors
|
||||
alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None
|
||||
binary_sensors: list[aioesphomeapi.BinarySensorInfo] = []
|
||||
|
||||
for entity in entities:
|
||||
if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo):
|
||||
alarm_info = entity
|
||||
elif isinstance(entity, aioesphomeapi.BinarySensorInfo):
|
||||
binary_sensors.append(entity)
|
||||
|
||||
assert alarm_info is not None, "Alarm control panel entity info not found"
|
||||
assert alarm_info.name == "Test Alarm"
|
||||
assert alarm_info.requires_code is True
|
||||
assert alarm_info.requires_code_to_arm is True
|
||||
|
||||
# Verify we have 10 binary sensors
|
||||
assert len(binary_sensors) == 10, (
|
||||
f"Expected 10 binary sensors, got {len(binary_sensors)}"
|
||||
)
|
||||
|
||||
# Verify sensor names
|
||||
expected_sensor_names = {
|
||||
"Door 1",
|
||||
"Door 2",
|
||||
"Window 1",
|
||||
"Window 2",
|
||||
"Motion 1",
|
||||
"Motion 2",
|
||||
"Glass Break 1",
|
||||
"Glass Break 2",
|
||||
"Smoke Detector",
|
||||
"CO Detector",
|
||||
}
|
||||
actual_sensor_names = {sensor.name for sensor in binary_sensors}
|
||||
assert actual_sensor_names == expected_sensor_names, (
|
||||
f"Sensor names mismatch. Expected: {expected_sensor_names}, "
|
||||
f"Got: {actual_sensor_names}"
|
||||
)
|
||||
|
||||
# Use InitialStateHelper to wait for all initial states
|
||||
state_helper = InitialStateHelper(entities)
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
# We'll receive subsequent states here after initial states
|
||||
pass
|
||||
|
||||
client.subscribe_states(state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states
|
||||
await state_helper.wait_for_initial_states(timeout=5.0)
|
||||
|
||||
# Verify the alarm state is disarmed initially
|
||||
alarm_state = state_helper.initial_states.get(alarm_info.key)
|
||||
assert alarm_state is not None, "Alarm control panel initial state not received"
|
||||
assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState)
|
||||
assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, (
|
||||
f"Expected initial state DISARMED, got {alarm_state.state}"
|
||||
)
|
||||
|
||||
# Verify all 10 binary sensors have initial states
|
||||
binary_sensor_states = [
|
||||
state_helper.initial_states.get(sensor.key) for sensor in binary_sensors
|
||||
]
|
||||
assert all(state is not None for state in binary_sensor_states), (
|
||||
"Not all binary sensors have initial states"
|
||||
)
|
||||
|
||||
# Verify all binary sensor states are BinarySensorState type
|
||||
for i, state in enumerate(binary_sensor_states):
|
||||
assert isinstance(state, aioesphomeapi.BinarySensorState), (
|
||||
f"Binary sensor {i} state is not BinarySensorState: {type(state)}"
|
||||
)
|
||||
|
||||
# Verify supported features
|
||||
expected_features = (
|
||||
EspHomeACPFeatures.ARM_HOME
|
||||
| EspHomeACPFeatures.ARM_AWAY
|
||||
| EspHomeACPFeatures.ARM_NIGHT
|
||||
| EspHomeACPFeatures.TRIGGER
|
||||
)
|
||||
assert alarm_info.supported_features == expected_features, (
|
||||
f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), "
|
||||
f"got {alarm_info.supported_features}"
|
||||
)
|
||||
@@ -1,286 +0,0 @@
|
||||
"""Tests for lambda deduplication in cpp_generator."""
|
||||
|
||||
from esphome import cpp_generator as cg
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_deduplicate_identical_lambdas() -> None:
|
||||
"""Test that identical stateless lambdas are deduplicated."""
|
||||
# Create two identical lambda expressions
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Try to deduplicate them
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Both should get the same function name (deduplication happened)
|
||||
assert func_name1 == func_name2
|
||||
assert func_name1 == "shared_lambda_0"
|
||||
|
||||
|
||||
def test_different_lambdas_not_deduplicated() -> None:
|
||||
"""Test that different lambdas get different function names."""
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return 24;"], # Different content
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Different lambdas should get different function names
|
||||
assert func_name1 != func_name2
|
||||
assert func_name1 == "shared_lambda_0"
|
||||
assert func_name2 == "shared_lambda_1"
|
||||
|
||||
|
||||
def test_different_return_types_not_deduplicated() -> None:
|
||||
"""Test that lambdas with different return types are not deduplicated."""
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return 42;"], # Same content
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("float"), # Different return type
|
||||
)
|
||||
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Different return types = different functions
|
||||
assert func_name1 != func_name2
|
||||
|
||||
|
||||
def test_different_parameters_not_deduplicated() -> None:
|
||||
"""Test that lambdas with different parameters are not deduplicated."""
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return x;"],
|
||||
parameters=[("int", "x")],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return x;"], # Same content
|
||||
parameters=[("float", "x")], # Different parameter type
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Different parameters = different functions
|
||||
assert func_name1 != func_name2
|
||||
|
||||
|
||||
def test_flush_lambda_dedup_declarations() -> None:
|
||||
"""Test that deferred declarations are properly stored for later flushing."""
|
||||
# Create a lambda which will create a deferred declaration
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
cg._get_shared_lambda_name(lambda1)
|
||||
|
||||
# Check that declaration was stored
|
||||
assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data
|
||||
assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1
|
||||
|
||||
# Verify the declaration content is correct
|
||||
declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0]
|
||||
assert "shared_lambda_0" in declaration
|
||||
assert "return 42;" in declaration
|
||||
|
||||
# Note: The actual flushing happens via CORE.add_job with FINAL priority
|
||||
# during real code generation, so we don't test that here
|
||||
|
||||
|
||||
def test_shared_function_lambda_expression() -> None:
|
||||
"""Test SharedFunctionLambdaExpression behaves correctly."""
|
||||
shared_lambda = cg.SharedFunctionLambdaExpression(
|
||||
func_name="shared_lambda_0",
|
||||
parameters=[],
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Should output just the function name
|
||||
assert str(shared_lambda) == "shared_lambda_0"
|
||||
|
||||
# Should have empty capture (stateless)
|
||||
assert shared_lambda.capture == ""
|
||||
|
||||
# Should have empty content (just a reference)
|
||||
assert shared_lambda.content == ""
|
||||
|
||||
|
||||
def test_lambda_deduplication_counter() -> None:
|
||||
"""Test that lambda counter increments correctly."""
|
||||
# Create 3 different lambdas
|
||||
for i in range(3):
|
||||
lambda_expr = cg.LambdaExpression(
|
||||
parts=[f"return {i};"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
func_name = cg._get_shared_lambda_name(lambda_expr)
|
||||
assert func_name == f"shared_lambda_{i}"
|
||||
|
||||
|
||||
def test_lambda_format_body() -> None:
|
||||
"""Test that format_body correctly formats lambda body with source."""
|
||||
# Without source
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=None,
|
||||
source=None,
|
||||
)
|
||||
assert lambda1.format_body() == "return 42;"
|
||||
|
||||
# With source would need a proper source object, skip for now
|
||||
|
||||
|
||||
def test_stateful_lambdas_not_deduplicated() -> None:
|
||||
"""Test that stateful lambdas (non-empty capture) are not deduplicated."""
|
||||
# _get_shared_lambda_name is only called for stateless lambdas (capture == "")
|
||||
# Stateful lambdas bypass deduplication entirely in process_lambda
|
||||
|
||||
# Verify that a stateful lambda would NOT get deduplicated
|
||||
# by checking it's not in the stateless dedup cache
|
||||
stateful_lambda = cg.LambdaExpression(
|
||||
parts=["return x + y;"],
|
||||
parameters=[],
|
||||
capture="=", # Non-empty capture means stateful
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Stateful lambdas should NOT be passed to _get_shared_lambda_name
|
||||
# This is enforced by the `if capture == ""` check in process_lambda
|
||||
# We verify the lambda has a non-empty capture
|
||||
assert stateful_lambda.capture != ""
|
||||
assert stateful_lambda.capture == "="
|
||||
|
||||
|
||||
def test_static_variable_detection() -> None:
|
||||
"""Test detection of static variables in lambda code."""
|
||||
# Should detect static variables
|
||||
assert cg._has_static_variables("static int counter = 0;")
|
||||
assert cg._has_static_variables("static bool flag = false; return flag;")
|
||||
assert cg._has_static_variables(" static float value = 1.0; ")
|
||||
|
||||
# Should NOT detect static_cast, static_assert, etc. (with underscores)
|
||||
assert not cg._has_static_variables("return static_cast<int>(value);")
|
||||
assert not cg._has_static_variables("static_assert(sizeof(int) == 4);")
|
||||
assert not cg._has_static_variables("auto ptr = static_pointer_cast<Foo>(bar);")
|
||||
|
||||
# Edge case: 'cast', 'assert', 'pointer_cast' are NOT C++ keywords
|
||||
# Someone could use them as type names, but we should NOT flag them
|
||||
# because they're not actually static variables with state
|
||||
# NOTE: These are valid C++ but extremely unlikely in ESPHome lambdas
|
||||
assert not cg._has_static_variables("static cast obj;") # 'cast' as type name
|
||||
assert not cg._has_static_variables("static assert value;") # 'assert' as type name
|
||||
assert not cg._has_static_variables(
|
||||
"static pointer_cast ptr;"
|
||||
) # 'pointer_cast' as type
|
||||
|
||||
# Should NOT detect in comments
|
||||
assert not cg._has_static_variables("// static int x = 0;\nreturn 42;")
|
||||
assert not cg._has_static_variables("/* static int y = 0; */ return 42;")
|
||||
|
||||
# Should detect even with comments elsewhere
|
||||
assert cg._has_static_variables("// comment\nstatic int x = 0;\nreturn x;")
|
||||
|
||||
# Should NOT detect non-static code
|
||||
assert not cg._has_static_variables("int counter = 0; return counter++;")
|
||||
assert not cg._has_static_variables("return 42;")
|
||||
|
||||
# Should handle newlines between static and type/variable
|
||||
assert cg._has_static_variables("static int\nfoo = 0;")
|
||||
assert cg._has_static_variables("static\nint\nbar = 0;")
|
||||
assert cg._has_static_variables(
|
||||
"static int \n foo = 0;"
|
||||
) # Mixed spaces/newlines
|
||||
|
||||
|
||||
def test_lambdas_with_static_not_deduplicated() -> None:
|
||||
"""Test that lambdas with static variables are not deduplicated."""
|
||||
# Two identical lambdas with static variables
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["static int counter = 0; return counter++;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["static int counter = 0; return counter++;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Should return None (not deduplicated)
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
assert func_name1 is None
|
||||
assert func_name2 is None
|
||||
|
||||
|
||||
def test_lambdas_without_static_still_deduplicated() -> None:
|
||||
"""Test that lambdas without static variables are still deduplicated."""
|
||||
# Two identical lambdas WITHOUT static variables
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["int counter = 0; return counter++;"], # No static
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["int counter = 0; return counter++;"], # No static
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Should be deduplicated (same function name)
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
assert func_name1 is not None
|
||||
assert func_name2 is not None
|
||||
assert func_name1 == func_name2
|
||||
@@ -355,7 +355,6 @@ def test_clean_build(
|
||||
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
|
||||
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
|
||||
mock_core.relative_build_path.return_value = dependencies_lock
|
||||
mock_core.platformio_cache_dir = str(platformio_cache_dir)
|
||||
|
||||
# Verify all exist before
|
||||
assert pioenvs_dir.exists()
|
||||
|
||||
Reference in New Issue
Block a user