mirror of
https://github.com/esphome/esphome.git
synced 2025-11-19 16:25:50 +00:00
Compare commits
25 Commits
bh1750_loo
...
2025.11.0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfd614c00c | ||
|
|
2681a14d05 | ||
|
|
f436f6ee2e | ||
|
|
f18bc62690 | ||
|
|
6db73df649 | ||
|
|
93215f1737 | ||
|
|
70aa94b8a4 | ||
|
|
e8998a79c7 | ||
|
|
3b25fdbc5f | ||
|
|
6c8577678c | ||
|
|
70366d2124 | ||
|
|
a38c4e0c6e | ||
|
|
6c6b03bda0 | ||
|
|
9e02e31917 | ||
|
|
3fd58f1a91 | ||
|
|
9151489481 | ||
|
|
f19296ac7f | ||
|
|
36868ee7b1 | ||
|
|
d559f9f52e | ||
|
|
6440b5fbf5 | ||
|
|
97c4914573 | ||
|
|
7ce94c27fe | ||
|
|
eb54c0026d | ||
|
|
fe00e209ff | ||
|
|
aed80732f9 |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.5
|
||||
rev: v0.14.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.12.0-dev
|
||||
PROJECT_NUMBER = 2025.11.0b4
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "bh1750.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::bh1750 {
|
||||
namespace esphome {
|
||||
namespace bh1750 {
|
||||
|
||||
static const char *const TAG = "bh1750.sensor";
|
||||
|
||||
@@ -13,31 +13,6 @@ 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:
|
||||
|
||||
@@ -68,7 +43,74 @@ void BH1750Sensor::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->state_ = IDLE;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
void BH1750Sensor::dump_config() {
|
||||
@@ -82,189 +124,45 @@ void BH1750Sensor::dump_config() {
|
||||
}
|
||||
|
||||
void BH1750Sensor::update() {
|
||||
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");
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) {
|
||||
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;
|
||||
}
|
||||
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
|
||||
// fetch millis() again since the read can take a bit
|
||||
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) {
|
||||
this->fail_and_reset_();
|
||||
break;
|
||||
}
|
||||
|
||||
this->state_ = WAITING_FINE_MEASUREMENT;
|
||||
break;
|
||||
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);
|
||||
|
||||
case WAITING_FINE_MEASUREMENT:
|
||||
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
|
||||
this->state_ = READING_FINE_RESULT;
|
||||
this->read_lx_(use_mode, use_mtreg, [this](float val) {
|
||||
if (std::isnan(val)) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
|
||||
this->status_clear_warning();
|
||||
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;
|
||||
this->publish_state(val);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace esphome::bh1750
|
||||
} // namespace bh1750
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::bh1750 {
|
||||
namespace esphome {
|
||||
namespace bh1750 {
|
||||
|
||||
enum BH1750Mode : uint8_t {
|
||||
enum BH1750Mode {
|
||||
BH1750_MODE_L,
|
||||
BH1750_MODE_H,
|
||||
BH1750_MODE_H2,
|
||||
@@ -20,36 +21,13 @@ 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:
|
||||
// State machine states
|
||||
enum State : uint8_t {
|
||||
IDLE,
|
||||
WAITING_COARSE_MEASUREMENT,
|
||||
READING_COARSE_RESULT,
|
||||
WAITING_FINE_MEASUREMENT,
|
||||
READING_FINE_RESULT,
|
||||
};
|
||||
void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
|
||||
|
||||
// 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 esphome::bh1750
|
||||
} // namespace bh1750
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -66,7 +67,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
@@ -78,7 +79,7 @@ void MultiClickTrigger::schedule_cooldown_() {
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -89,19 +90,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::cancel() {
|
||||
void binary_sensor::MultiClickTrigger::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void MultiClickTrigger::trigger_() {
|
||||
void binary_sensor::MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout("trigger");
|
||||
@@ -117,4 +118,5 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
|
||||
struct MultiClickTriggerEvent {
|
||||
bool state;
|
||||
@@ -171,4 +172,5 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
@@ -61,4 +63,6 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
}
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
|
||||
@@ -68,4 +70,5 @@ class BinarySensorInitiallyOff : public BinarySensor {
|
||||
bool has_state() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "binary_sensor.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
@@ -130,4 +132,6 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
|
||||
@@ -137,4 +139,6 @@ class SettleFilter : public Filter, public Component {
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType:
|
||||
"Add 'ap:' to your WiFi configuration to enable the captive portal."
|
||||
)
|
||||
|
||||
# Register socket needs for DNS server and additional HTTP connections
|
||||
# - 1 UDP socket for DNS server
|
||||
# - 3 additional TCP sockets for captive portal detection probes + configuration requests
|
||||
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
|
||||
# Need headroom for actual user configuration requests.
|
||||
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(4, "captive_portal")(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
ESP_LOGI(TAG, "Requested WiFi Settings Change:");
|
||||
ESP_LOGI(TAG, " SSID='%s'", ssid.c_str());
|
||||
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
||||
wifi::global_wifi_component->start_scanning();
|
||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler(this);
|
||||
#ifdef USE_ESP32
|
||||
// Enable LRU socket purging to handle captive portal detection probe bursts
|
||||
// OS captive portal detection makes many simultaneous HTTP requests which can
|
||||
// exhaust sockets. LRU purging automatically closes oldest idle connections.
|
||||
this->base_->get_server()->set_lru_purge_enable(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
@@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
void end() {
|
||||
this->active_ = false;
|
||||
this->disable_loop(); // Stop processing DNS requests
|
||||
#ifdef USE_ESP32
|
||||
// Disable LRU socket purging now that captive portal is done
|
||||
this->base_->get_server()->set_lru_purge_enable(false);
|
||||
#endif
|
||||
this->base_->deinit();
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->stop();
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cover.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
template<typename... Ts> class OpenAction : public Action<Ts...> {
|
||||
public:
|
||||
@@ -130,4 +131,5 @@ class CoverClosedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
static const char *const TAG = "cover";
|
||||
|
||||
@@ -211,4 +212,5 @@ void CoverRestoreState::apply(Cover *cover) {
|
||||
cover->publish_state();
|
||||
}
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
#include "cover_traits.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
const extern float COVER_OPEN;
|
||||
const extern float COVER_CLOSED;
|
||||
@@ -156,4 +157,5 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
||||
ESPPreferenceObject rtc_;
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
class CoverTraits {
|
||||
public:
|
||||
@@ -25,4 +26,5 @@ class CoverTraits {
|
||||
bool supports_stop_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -931,6 +931,12 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||
|
||||
@@ -634,13 +634,11 @@ void ESP32BLE::dump_config() {
|
||||
io_capability_s = "invalid";
|
||||
break;
|
||||
}
|
||||
char mac_s[18];
|
||||
format_mac_addr_upper(mac_address, mac_s);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"BLE:\n"
|
||||
" MAC address: %s\n"
|
||||
" IO Capability: %s",
|
||||
mac_s, io_capability_s);
|
||||
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ CONF_ON_STOP = "on_stop"
|
||||
CONF_STATUS_INDICATOR = "status_indicator"
|
||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
||||
|
||||
# Default WiFi timeout - aligned with WiFi component ap_timeout
|
||||
# Allows sufficient time to try all BSSIDs before starting provisioning mode
|
||||
DEFAULT_WIFI_TIMEOUT = "90s"
|
||||
|
||||
|
||||
improv_ns = cg.esphome_ns.namespace("improv")
|
||||
Error = improv_ns.enum("Error")
|
||||
@@ -59,7 +63,7 @@ CONFIG_SCHEMA = (
|
||||
CONF_AUTHORIZED_DURATION, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default="1min"
|
||||
CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
|
||||
@@ -127,6 +127,7 @@ void ESP32ImprovComponent::loop() {
|
||||
// Set initial state based on whether we have an authorizer
|
||||
this->set_state_(this->get_initial_state_(), false);
|
||||
this->set_error_(improv::ERROR_NONE);
|
||||
this->should_start_ = false; // Clear flag after starting
|
||||
ESP_LOGD(TAG, "Service started!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||
void start();
|
||||
void stop();
|
||||
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
|
||||
bool should_start() const { return this->should_start_; }
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
|
||||
|
||||
@@ -36,6 +36,7 @@ from esphome.const import (
|
||||
CONF_WEIGHT,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.helpers import cpp_string_escape
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -49,6 +50,7 @@ font_ns = cg.esphome_ns.namespace("font")
|
||||
|
||||
Font = font_ns.class_("Font")
|
||||
Glyph = font_ns.class_("Glyph")
|
||||
GlyphData = font_ns.struct("GlyphData")
|
||||
|
||||
CONF_BPP = "bpp"
|
||||
CONF_EXTRAS = "extras"
|
||||
@@ -461,7 +463,7 @@ FONT_SCHEMA = cv.Schema(
|
||||
)
|
||||
),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -486,8 +488,6 @@ class GlyphInfo:
|
||||
|
||||
|
||||
def glyph_to_glyphinfo(glyph, font, size, bpp):
|
||||
# Convert to 32 bit unicode codepoint
|
||||
glyph = ord(glyph)
|
||||
scale = 256 // (1 << bpp)
|
||||
if not font.is_scalable:
|
||||
sizes = [pt_to_px(x.size) for x in font.available_sizes]
|
||||
@@ -583,15 +583,22 @@ async def to_code(config):
|
||||
|
||||
# Create the glyph table that points to data in the above array.
|
||||
glyph_initializer = [
|
||||
[
|
||||
x.glyph,
|
||||
prog_arr + (y - len(x.bitmap_data)),
|
||||
x.advance,
|
||||
x.offset_x,
|
||||
x.offset_y,
|
||||
x.width,
|
||||
x.height,
|
||||
]
|
||||
cg.StructInitializer(
|
||||
GlyphData,
|
||||
(
|
||||
"a_char",
|
||||
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
|
||||
),
|
||||
(
|
||||
"data",
|
||||
cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
|
||||
),
|
||||
("advance", x.advance),
|
||||
("offset_x", x.offset_x),
|
||||
("offset_y", x.offset_y),
|
||||
("width", x.width),
|
||||
("height", x.height),
|
||||
)
|
||||
for (x, y) in zip(
|
||||
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
|
||||
)
|
||||
|
||||
@@ -6,245 +6,133 @@
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
static const char *const TAG = "font";
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
|
||||
auto *fe = (Font *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
||||
if (gd == nullptr) {
|
||||
return nullptr;
|
||||
const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; }
|
||||
// Compare the char at the string position with this char.
|
||||
// Return true if this char is less than or equal the other.
|
||||
bool Glyph::compare_to(const uint8_t *str) const {
|
||||
// 1 -> this->char_
|
||||
// 2 -> str
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return true;
|
||||
if (str[i] == '\0')
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] > str[i])
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] < str[i])
|
||||
return true;
|
||||
}
|
||||
return gd->data;
|
||||
// this should not happen
|
||||
return false;
|
||||
}
|
||||
int Glyph::match_length(const uint8_t *str) const {
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return i;
|
||||
if (str[i] != this->glyph_data_->a_char[i])
|
||||
return 0;
|
||||
}
|
||||
// this should not happen
|
||||
return 0;
|
||||
}
|
||||
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||
*x1 = this->glyph_data_->offset_x;
|
||||
*y1 = this->glyph_data_->offset_y;
|
||||
*width = this->glyph_data_->width;
|
||||
*height = this->glyph_data_->height;
|
||||
}
|
||||
|
||||
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
|
||||
auto *fe = (Font *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
||||
if (gd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
dsc->adv_w = gd->advance;
|
||||
dsc->ofs_x = gd->offset_x;
|
||||
dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line;
|
||||
dsc->box_w = gd->width;
|
||||
dsc->box_h = gd->height;
|
||||
dsc->is_placeholder = 0;
|
||||
dsc->bpp = fe->get_bpp();
|
||||
return true;
|
||||
}
|
||||
|
||||
const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) {
|
||||
if (unicode_letter == this->last_letter_ && this->last_letter_ != 0)
|
||||
return this->last_data_;
|
||||
auto *glyph = this->find_glyph(unicode_letter);
|
||||
if (glyph == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
this->last_data_ = glyph;
|
||||
this->last_letter_ = unicode_letter;
|
||||
return glyph;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string.
|
||||
* If successful, return the codepoint and set the length to the number of bytes read.
|
||||
* If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to
|
||||
* 0.
|
||||
*
|
||||
* @param utf8_str The input string
|
||||
* @param length Pointer to length storage
|
||||
* @return The extracted code point
|
||||
*/
|
||||
static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
|
||||
// Safely cast to uint8_t* for correct bitwise operations on bytes
|
||||
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
uint32_t code_point = 0;
|
||||
uint8_t c1 = *current++;
|
||||
|
||||
// check for end of string
|
||||
if (c1 == 0) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
|
||||
if (c1 < 0x80) {
|
||||
// Valid ASCII byte.
|
||||
code_point = c1;
|
||||
// Optimization: No need to check for continuation bytes.
|
||||
}
|
||||
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xE0) == 0xC0) {
|
||||
uint8_t c2 = *current++;
|
||||
|
||||
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
|
||||
if ((c2 & 0xC0) != 0x80) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x1F) << 6;
|
||||
code_point |= (c2 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (2-byte must be > 0x7F)
|
||||
if (code_point <= 0x7F) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF0) == 0xE0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x0F) << 12;
|
||||
code_point |= (c2 & 0x3F) << 6;
|
||||
code_point |= (c3 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
|
||||
// Also check for surrogates (0xD800-0xDFFF)
|
||||
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF8) == 0xF0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
uint8_t c4 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x07) << 18;
|
||||
code_point |= (c2 & 0x3F) << 12;
|
||||
code_point |= (c3 & 0x3F) << 6;
|
||||
code_point |= (c4 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
|
||||
// Also check for valid Unicode range (must be <= 0x10FFFF)
|
||||
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
|
||||
else {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
return code_point;
|
||||
}
|
||||
|
||||
Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp)
|
||||
: glyphs_(ConstVector(data, data_nr)),
|
||||
baseline_(baseline),
|
||||
: baseline_(baseline),
|
||||
height_(height),
|
||||
descender_(descender),
|
||||
linegap_(height - baseline - descender),
|
||||
xheight_(xheight),
|
||||
capheight_(capheight),
|
||||
bpp_(bpp) {
|
||||
#ifdef USE_LVGL_FONT
|
||||
this->lv_font_.dsc = this;
|
||||
this->lv_font_.line_height = this->get_height();
|
||||
this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline();
|
||||
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
|
||||
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
|
||||
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
|
||||
this->lv_font_.underline_position = -1;
|
||||
this->lv_font_.underline_thickness = 1;
|
||||
#endif
|
||||
glyphs_.reserve(data_nr);
|
||||
for (int i = 0; i < data_nr; ++i)
|
||||
glyphs_.emplace_back(&data[i]);
|
||||
}
|
||||
|
||||
const Glyph *Font::find_glyph(uint32_t codepoint) const {
|
||||
int Font::match_next_glyph(const uint8_t *str, int *match_length) {
|
||||
int lo = 0;
|
||||
int hi = this->glyphs_.size() - 1;
|
||||
while (lo != hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (this->glyphs_[mid].is_less_or_equal(codepoint)) {
|
||||
if (this->glyphs_[mid].compare_to(str)) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
auto *result = &this->glyphs_[lo];
|
||||
if (result->code_point == codepoint)
|
||||
return result;
|
||||
return nullptr;
|
||||
*match_length = this->glyphs_[lo].match_length(str);
|
||||
if (*match_length <= 0)
|
||||
return -1;
|
||||
return lo;
|
||||
}
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
|
||||
*baseline = this->baseline_;
|
||||
*height = this->height_;
|
||||
int i = 0;
|
||||
int min_x = 0;
|
||||
bool has_char = false;
|
||||
int x = 0;
|
||||
for (;;) {
|
||||
size_t length;
|
||||
auto code_point = extract_unicode_codepoint(str, &length);
|
||||
if (length == 0)
|
||||
break;
|
||||
str += length;
|
||||
auto *glyph = this->find_glyph(code_point);
|
||||
if (glyph == nullptr) {
|
||||
while (str[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
if (!this->glyphs_.empty())
|
||||
x += this->glyphs_[0].advance;
|
||||
if (!this->get_glyphs().empty())
|
||||
x += this->get_glyphs()[0].glyph_data_->advance;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->glyphs_[glyph_n];
|
||||
if (!has_char) {
|
||||
min_x = glyph->offset_x;
|
||||
min_x = glyph.glyph_data_->offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph->offset_x);
|
||||
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
|
||||
}
|
||||
x += glyph->advance;
|
||||
x += glyph.glyph_data_->advance;
|
||||
|
||||
i += match_length;
|
||||
has_char = true;
|
||||
}
|
||||
*x_offset = min_x;
|
||||
*width = x - min_x;
|
||||
}
|
||||
|
||||
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) {
|
||||
int i = 0;
|
||||
int x_at = x_start;
|
||||
for (;;) {
|
||||
size_t length;
|
||||
auto code_point = extract_unicode_codepoint(text, &length);
|
||||
if (length == 0)
|
||||
break;
|
||||
text += length;
|
||||
auto *glyph = this->find_glyph(code_point);
|
||||
if (glyph == nullptr) {
|
||||
int scan_x1, scan_y1, scan_width, scan_height;
|
||||
while (text[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
|
||||
if (!this->glyphs_.empty()) {
|
||||
uint8_t glyph_width = this->glyphs_[0].advance;
|
||||
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
|
||||
if (!this->get_glyphs().empty()) {
|
||||
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
|
||||
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
x_at += glyph_width;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t *data = glyph->data;
|
||||
const int max_x = x_at + glyph->offset_x + glyph->width;
|
||||
const int max_y = y_start + glyph->offset_y + glyph->height;
|
||||
const Glyph &glyph = this->get_glyphs()[glyph_n];
|
||||
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||
|
||||
const uint8_t *data = glyph.glyph_data_->data;
|
||||
const int max_x = x_at + scan_x1 + scan_width;
|
||||
const int max_y = y_start + scan_y1 + scan_height;
|
||||
|
||||
uint8_t bitmask = 0;
|
||||
uint8_t pixel_data = 0;
|
||||
@@ -257,10 +145,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
auto b_g = (float) background.g;
|
||||
auto b_b = (float) background.b;
|
||||
auto b_w = (float) background.w;
|
||||
for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) {
|
||||
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
|
||||
uint8_t pixel = 0;
|
||||
for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) {
|
||||
for (int bit_num = 0; bit_num != this->bpp_; bit_num++) {
|
||||
if (bitmask == 0) {
|
||||
pixel_data = progmem_read_byte(data++);
|
||||
bitmask = 0x80;
|
||||
@@ -280,9 +168,12 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
}
|
||||
}
|
||||
}
|
||||
x_at += glyph->advance;
|
||||
x_at += glyph.glyph_data_->advance;
|
||||
|
||||
i += match_length;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace font
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,30 +6,14 @@
|
||||
#ifdef USE_DISPLAY
|
||||
#include "esphome/components/display/display.h"
|
||||
#endif
|
||||
#ifdef USE_LVGL_FONT
|
||||
#include <lvgl.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
class Font;
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
|
||||
int height)
|
||||
: code_point(code_point),
|
||||
data(data),
|
||||
advance(advance),
|
||||
offset_x(offset_x),
|
||||
offset_y(offset_y),
|
||||
width(width),
|
||||
height(height) {}
|
||||
|
||||
bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; }
|
||||
|
||||
const uint32_t code_point;
|
||||
struct GlyphData {
|
||||
const uint8_t *a_char;
|
||||
const uint8_t *data;
|
||||
int advance;
|
||||
int offset_x;
|
||||
@@ -38,6 +22,26 @@ class Glyph {
|
||||
int height;
|
||||
};
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
||||
|
||||
const uint8_t *get_char() const;
|
||||
|
||||
bool compare_to(const uint8_t *str) const;
|
||||
|
||||
int match_length(const uint8_t *str) const;
|
||||
|
||||
void scan_area(int *x1, int *y1, int *width, int *height) const;
|
||||
|
||||
const GlyphData *get_glyph_data() const { return this->glyph_data_; }
|
||||
|
||||
protected:
|
||||
friend Font;
|
||||
|
||||
const GlyphData *glyph_data_;
|
||||
};
|
||||
|
||||
class Font
|
||||
#ifdef USE_DISPLAY
|
||||
: public display::BaseFont
|
||||
@@ -46,8 +50,8 @@ class Font
|
||||
public:
|
||||
/** Construct the font with the given glyphs.
|
||||
*
|
||||
* @param data A list of glyphs, must be sorted lexicographically.
|
||||
* @param data_nr The number of glyphs
|
||||
* @param data A vector of glyphs, must be sorted lexicographically.
|
||||
* @param data_nr The number of glyphs in data.
|
||||
* @param baseline The y-offset from the top of the text to the baseline.
|
||||
* @param height The y-offset from the top of the text to the bottom.
|
||||
* @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
|
||||
@@ -55,10 +59,10 @@ class Font
|
||||
* @param capheight The height of capital letters, usually measured at the "X" glyph.
|
||||
* @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
|
||||
*/
|
||||
Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp = 1);
|
||||
|
||||
const Glyph *find_glyph(uint32_t codepoint) const;
|
||||
int match_next_glyph(const uint8_t *str, int *match_length);
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
|
||||
@@ -73,14 +77,11 @@ class Font
|
||||
inline int get_xheight() { return this->xheight_; }
|
||||
inline int get_capheight() { return this->capheight_; }
|
||||
inline int get_bpp() { return this->bpp_; }
|
||||
#ifdef USE_LVGL_FONT
|
||||
const lv_font_t *get_lv_font() const { return &this->lv_font_; }
|
||||
#endif
|
||||
|
||||
const ConstVector<Glyph> &get_glyphs() const { return glyphs_; }
|
||||
const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
|
||||
|
||||
protected:
|
||||
ConstVector<Glyph> glyphs_;
|
||||
std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
|
||||
int baseline_;
|
||||
int height_;
|
||||
int descender_;
|
||||
@@ -88,14 +89,6 @@ class Font
|
||||
int xheight_;
|
||||
int capheight_;
|
||||
uint8_t bpp_; // bits per pixel
|
||||
#ifdef USE_LVGL_FONT
|
||||
lv_font_t lv_font_{};
|
||||
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
|
||||
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
|
||||
const Glyph *get_glyph_data_(uint32_t unicode_letter);
|
||||
uint32_t last_letter_{};
|
||||
const Glyph *last_data_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace font
|
||||
|
||||
@@ -52,7 +52,15 @@ from .schemas import (
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
from .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||
from .trigger import add_on_boot_triggers, generate_triggers
|
||||
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
|
||||
from .types import (
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
PlainTrigger,
|
||||
lv_font_t,
|
||||
lv_group_t,
|
||||
lv_style_t,
|
||||
lvgl_ns,
|
||||
)
|
||||
from .widgets import (
|
||||
LvScrActType,
|
||||
Widget,
|
||||
@@ -236,6 +244,7 @@ async def to_code(configs):
|
||||
cg.add_global(lvgl_ns.using)
|
||||
for font in helpers.esphome_fonts_used:
|
||||
await cg.get_variable(font)
|
||||
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
|
||||
default_font = config_0[df.CONF_DEFAULT_FONT]
|
||||
if not lvalid.is_lv_font(default_font):
|
||||
add_define(
|
||||
@@ -247,8 +256,7 @@ async def to_code(configs):
|
||||
type=lv_font_t.operator("ptr").operator("const"),
|
||||
)
|
||||
cg.new_variable(
|
||||
globfont_id,
|
||||
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
|
||||
globfont_id, MockObj(await lvalid.lv_font.process(default_font))
|
||||
)
|
||||
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
|
||||
else:
|
||||
|
||||
76
esphome/components/lvgl/font.cpp
Normal file
76
esphome/components/lvgl/font.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "lvgl_esphome.h"
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
namespace esphome {
|
||||
namespace lvgl {
|
||||
|
||||
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
|
||||
auto *fe = (FontEngine *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data(unicode_letter);
|
||||
if (gd == nullptr)
|
||||
return nullptr;
|
||||
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
|
||||
|
||||
return gd->data;
|
||||
}
|
||||
|
||||
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
|
||||
auto *fe = (FontEngine *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data(unicode_letter);
|
||||
if (gd == nullptr)
|
||||
return false;
|
||||
dsc->adv_w = gd->advance;
|
||||
dsc->ofs_x = gd->offset_x;
|
||||
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
|
||||
dsc->box_w = gd->width;
|
||||
dsc->box_h = gd->height;
|
||||
dsc->is_placeholder = 0;
|
||||
dsc->bpp = fe->bpp;
|
||||
return true;
|
||||
}
|
||||
|
||||
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
|
||||
this->bpp = esp_font->get_bpp();
|
||||
this->lv_font_.dsc = this;
|
||||
this->lv_font_.line_height = this->height = esp_font->get_height();
|
||||
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
|
||||
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
|
||||
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
|
||||
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
|
||||
this->lv_font_.underline_position = -1;
|
||||
this->lv_font_.underline_thickness = 1;
|
||||
}
|
||||
|
||||
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
|
||||
|
||||
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
|
||||
if (unicode_letter == last_letter_)
|
||||
return this->last_data_;
|
||||
uint8_t unicode[5];
|
||||
memset(unicode, 0, sizeof unicode);
|
||||
if (unicode_letter > 0xFFFF) {
|
||||
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
|
||||
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
|
||||
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
|
||||
unicode[3] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else if (unicode_letter > 0x7FF) {
|
||||
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
|
||||
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
|
||||
unicode[2] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else if (unicode_letter > 0x7F) {
|
||||
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
|
||||
unicode[1] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else {
|
||||
unicode[0] = unicode_letter;
|
||||
}
|
||||
int match_length;
|
||||
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
|
||||
if (glyph_n < 0)
|
||||
return nullptr;
|
||||
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
|
||||
this->last_letter_ = unicode_letter;
|
||||
return this->last_data_;
|
||||
}
|
||||
} // namespace lvgl
|
||||
} // namespace esphome
|
||||
#endif // USES_LVGL_FONT
|
||||
@@ -493,7 +493,6 @@ class LvFont(LValidator):
|
||||
return LV_FONTS
|
||||
if is_lv_font(value):
|
||||
return lv_builtin_font(value)
|
||||
add_lv_use("font")
|
||||
fontval = cv.use_id(Font)(value)
|
||||
esphome_fonts_used.add(fontval)
|
||||
return requires_component("font")(fontval)
|
||||
@@ -503,9 +502,7 @@ class LvFont(LValidator):
|
||||
async def process(self, value, args=()):
|
||||
if is_lv_font(value):
|
||||
return literal(f"&lv_font_{value}")
|
||||
if isinstance(value, str):
|
||||
return literal(f"{value}")
|
||||
return await super().process(value, args)
|
||||
return literal(f"{value}_engine->get_lv_font()")
|
||||
|
||||
|
||||
lv_font = LvFont()
|
||||
|
||||
@@ -50,14 +50,6 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
|
||||
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
|
||||
#endif // LV_COLOR_DEPTH
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) {
|
||||
lv_obj_set_style_text_font(obj, font->get_lv_font(), part);
|
||||
}
|
||||
inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
lv_style_set_text_font(style, font->get_lv_font());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
// Shortcut / overload, so that the source of an image can easily be updated
|
||||
// from within a lambda.
|
||||
@@ -142,6 +134,24 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
||||
protected:
|
||||
std::function<void(Ts...)> lamb_;
|
||||
};
|
||||
#ifdef USE_LVGL_FONT
|
||||
class FontEngine {
|
||||
public:
|
||||
FontEngine(font::Font *esp_font);
|
||||
const lv_font_t *get_lv_font();
|
||||
|
||||
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
|
||||
uint16_t baseline{};
|
||||
uint16_t height{};
|
||||
uint8_t bpp{};
|
||||
|
||||
protected:
|
||||
font::Font *font_{};
|
||||
uint32_t last_letter_{};
|
||||
const font::GlyphData *last_data_{};
|
||||
lv_font_t lv_font_{};
|
||||
};
|
||||
#endif // USE_LVGL_FONT
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
void lv_animimg_stop(lv_obj_t *obj);
|
||||
#endif // USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -45,6 +45,7 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t")
|
||||
lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
|
||||
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
|
||||
lv_key_t = cg.global_ns.enum("lv_key_t")
|
||||
FontEngine = lvgl_ns.class_("FontEngine")
|
||||
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
|
||||
DrawEndTrigger = esphome_ns.class_(
|
||||
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -114,7 +115,9 @@ class SpinboxType(WidgetType):
|
||||
w.obj, digits, digits - config[CONF_DECIMAL_PLACES]
|
||||
)
|
||||
if (value := config.get(CONF_VALUE)) is not None:
|
||||
lv.spinbox_set_value(w.obj, await lv_float.process(value))
|
||||
lv.spinbox_set_value(
|
||||
w.obj, MockObj(await lv_float.process(value)) * w.get_scale()
|
||||
)
|
||||
|
||||
def get_scale(self, config):
|
||||
return 10 ** config[CONF_DECIMAL_PLACES]
|
||||
|
||||
@@ -350,6 +350,7 @@ void MipiRgb::dump_config() {
|
||||
"\n Width: %u"
|
||||
"\n Height: %u"
|
||||
"\n Rotation: %d degrees"
|
||||
"\n PCLK Inverted: %s"
|
||||
"\n HSync Pulse Width: %u"
|
||||
"\n HSync Back Porch: %u"
|
||||
"\n HSync Front Porch: %u"
|
||||
@@ -357,18 +358,18 @@ void MipiRgb::dump_config() {
|
||||
"\n VSync Back Porch: %u"
|
||||
"\n VSync Front Porch: %u"
|
||||
"\n Invert Colors: %s"
|
||||
"\n Pixel Clock: %dMHz"
|
||||
"\n Pixel Clock: %uMHz"
|
||||
"\n Reset Pin: %s"
|
||||
"\n DE Pin: %s"
|
||||
"\n PCLK Pin: %s"
|
||||
"\n HSYNC Pin: %s"
|
||||
"\n VSYNC Pin: %s",
|
||||
this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_,
|
||||
this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_,
|
||||
this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000,
|
||||
get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(),
|
||||
get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(),
|
||||
get_pin_name(this->vsync_pin_).c_str());
|
||||
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
|
||||
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
|
||||
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
|
||||
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
|
||||
|
||||
if (this->madctl_ & MADCTL_BGR) {
|
||||
this->dump_pins_(8, 13, "Blue", 0);
|
||||
|
||||
@@ -11,6 +11,7 @@ st7701s.extend(
|
||||
vsync_pin=17,
|
||||
pclk_pin=21,
|
||||
pclk_frequency="12MHz",
|
||||
pclk_inverted=False,
|
||||
pixel_mode="18bit",
|
||||
mirror_x=True,
|
||||
mirror_y=True,
|
||||
|
||||
@@ -118,10 +118,10 @@ struct IPAddress {
|
||||
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
|
||||
#endif
|
||||
|
||||
bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
|
||||
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
|
||||
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
|
||||
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
|
||||
bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
|
||||
bool is_ip4() { return IP_IS_V4(&ip_addr_); }
|
||||
bool is_ip6() { return IP_IS_V6(&ip_addr_); }
|
||||
bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); }
|
||||
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
|
||||
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
|
||||
@@ -103,7 +103,6 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
|
||||
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
||||
|
||||
CONF_DFU = "dfu"
|
||||
CONF_DCDC = "dcdc"
|
||||
CONF_REG0 = "reg0"
|
||||
CONF_UICR_ERASE = "uicr_erase"
|
||||
|
||||
@@ -122,7 +121,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_DCDC, default=True): cv.boolean,
|
||||
cv.Optional(CONF_REG0): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VOLTAGE): cv.All(
|
||||
@@ -198,7 +196,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if dfu_config := config.get(CONF_DFU):
|
||||
CORE.add_job(_dfu_to_code, dfu_config)
|
||||
zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC])
|
||||
|
||||
if reg0_config := config.get(CONF_REG0):
|
||||
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
|
||||
|
||||
@@ -73,17 +73,17 @@ void SFA30Component::update() {
|
||||
}
|
||||
|
||||
if (this->formaldehyde_sensor_ != nullptr) {
|
||||
const float formaldehyde = raw_data[0] / 5.0f;
|
||||
const float formaldehyde = static_cast<int16_t>(raw_data[0]) / 5.0f;
|
||||
this->formaldehyde_sensor_->publish_state(formaldehyde);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
const float humidity = raw_data[1] / 100.0f;
|
||||
const float humidity = static_cast<int16_t>(raw_data[1]) / 100.0f;
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
const float temperature = raw_data[2] / 200.0f;
|
||||
const float temperature = static_cast<int16_t>(raw_data[2]) / 200.0f;
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,18 @@ void AsyncWebServer::end() {
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServer::set_lru_purge_enable(bool enable) {
|
||||
if (this->lru_purge_enable_ == enable) {
|
||||
return; // No change needed
|
||||
}
|
||||
this->lru_purge_enable_ = enable;
|
||||
// If server is already running, restart it with new config
|
||||
if (this->server_) {
|
||||
this->end();
|
||||
this->begin();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServer::begin() {
|
||||
if (this->server_) {
|
||||
this->end();
|
||||
@@ -101,6 +113,8 @@ void AsyncWebServer::begin() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = this->port_;
|
||||
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
|
||||
// Enable LRU purging if requested (e.g., by captive portal to handle probe bursts)
|
||||
config.lru_purge_enable = this->lru_purge_enable_;
|
||||
if (httpd_start(&this->server_, &config) == ESP_OK) {
|
||||
const httpd_uri_t handler_get = {
|
||||
.uri = "",
|
||||
@@ -242,6 +256,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char
|
||||
void AsyncWebServerRequest::redirect(const std::string &url) {
|
||||
httpd_resp_set_status(*this, "302 Found");
|
||||
httpd_resp_set_hdr(*this, "Location", url.c_str());
|
||||
httpd_resp_set_hdr(*this, "Connection", "close");
|
||||
httpd_resp_send(*this, nullptr, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -199,9 +199,13 @@ class AsyncWebServer {
|
||||
return *handler;
|
||||
}
|
||||
|
||||
void set_lru_purge_enable(bool enable);
|
||||
httpd_handle_t get_server() { return this->server_; }
|
||||
|
||||
protected:
|
||||
uint16_t port_{};
|
||||
httpd_handle_t server_{};
|
||||
bool lru_purge_enable_{false};
|
||||
static esp_err_t request_handler(httpd_req_t *r);
|
||||
static esp_err_t request_post_handler(httpd_req_t *r);
|
||||
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
||||
|
||||
@@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode"
|
||||
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
||||
MAX_WIFI_NETWORKS = 127
|
||||
|
||||
# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection
|
||||
# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only
|
||||
# get best-effort connection attempts. Longer timeout ensures we exhaust all options
|
||||
# before falling back to AP mode. Aligned with improv wifi_timeout default.
|
||||
DEFAULT_AP_TIMEOUT = "90s"
|
||||
|
||||
wifi_ns = cg.esphome_ns.namespace("wifi")
|
||||
EAPAuth = wifi_ns.struct("EAPAuth")
|
||||
ManualIP = wifi_ns.struct("ManualIP")
|
||||
@@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout"
|
||||
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_AP_TIMEOUT, default="1min"
|
||||
CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -199,7 +199,12 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
|
||||
|
||||
/// Cooldown duration in milliseconds after adapter restart or repeated failures
|
||||
/// Allows WiFi hardware to stabilize before next connection attempt
|
||||
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000;
|
||||
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
|
||||
|
||||
/// Cooldown duration when fallback AP is active and captive portal may be running
|
||||
/// Longer interval gives users time to configure WiFi without constant connection attempts
|
||||
/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown
|
||||
static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
|
||||
|
||||
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
@@ -275,7 +280,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) {
|
||||
// If we didn't scan this cycle, treat all networks as potentially hidden
|
||||
// Otherwise, only retry networks that weren't seen in the scan
|
||||
if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) {
|
||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
|
||||
return static_cast<int8_t>(i);
|
||||
}
|
||||
@@ -417,10 +424,6 @@ void WiFiComponent::start() {
|
||||
void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
// Enter cooldown state to allow WiFi hardware to stabilize after restart
|
||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
@@ -441,7 +444,16 @@ void WiFiComponent::loop() {
|
||||
switch (this->state_) {
|
||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||
if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) {
|
||||
// Skip cooldown if new credentials were provided while connecting
|
||||
if (this->skip_cooldown_next_cycle_) {
|
||||
this->skip_cooldown_next_cycle_ = false;
|
||||
this->check_connecting_finished();
|
||||
break;
|
||||
}
|
||||
// Use longer cooldown when captive portal/improv is active to avoid disrupting user config
|
||||
bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
|
||||
uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
|
||||
if (now - this->action_started_ > cooldown_duration) {
|
||||
// After cooldown we either restarted the adapter because of
|
||||
// a failure, or something tried to connect over and over
|
||||
// so we entered cooldown. In both cases we call
|
||||
@@ -495,7 +507,8 @@ void WiFiComponent::loop() {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
#ifdef USE_IMPROV
|
||||
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) {
|
||||
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() &&
|
||||
!esp32_improv::global_improv_component->should_start()) {
|
||||
if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
|
||||
if (this->wifi_mode_(true, {}))
|
||||
esp32_improv::global_improv_component->start();
|
||||
@@ -605,6 +618,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
|
||||
this->init_sta(1);
|
||||
this->add_sta(ap);
|
||||
this->selected_sta_index_ = 0;
|
||||
// When new credentials are set (e.g., from improv), skip cooldown to retry immediately
|
||||
this->skip_cooldown_next_cycle_ = true;
|
||||
}
|
||||
|
||||
WiFiAP WiFiComponent::build_params_for_current_phase_() {
|
||||
@@ -666,29 +681,40 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
||||
sta.set_ssid(ssid);
|
||||
sta.set_password(password);
|
||||
this->set_sta(sta);
|
||||
|
||||
// Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
|
||||
this->connect_soon_();
|
||||
}
|
||||
|
||||
void WiFiComponent::connect_soon_() {
|
||||
// Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) {
|
||||
ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
|
||||
this->retry_connect();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
// Log connection attempt at INFO level with priority
|
||||
char bssid_s[18];
|
||||
std::string bssid_formatted;
|
||||
int8_t priority = 0;
|
||||
|
||||
if (ap.get_bssid().has_value()) {
|
||||
format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s);
|
||||
bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data());
|
||||
priority = this->get_sta_priority(ap.get_bssid().value());
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
|
||||
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority,
|
||||
this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
|
||||
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"),
|
||||
priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
|
||||
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
ESP_LOGV(TAG, "Connection Params:");
|
||||
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
|
||||
if (ap.get_bssid().has_value()) {
|
||||
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
|
||||
ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str());
|
||||
} else {
|
||||
ESP_LOGV(TAG, " BSSID: Not Set");
|
||||
}
|
||||
@@ -797,8 +823,6 @@ const LogString *get_signal_bars(int8_t rssi) {
|
||||
|
||||
void WiFiComponent::print_connect_params_() {
|
||||
bssid_t bssid = wifi_bssid();
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
|
||||
if (this->is_disabled()) {
|
||||
@@ -821,9 +845,9 @@ void WiFiComponent::print_connect_params_() {
|
||||
" Gateway: %s\n"
|
||||
" DNS1: %s\n"
|
||||
" DNS2: %s",
|
||||
wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
|
||||
get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(),
|
||||
wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
|
||||
wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi,
|
||||
LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(),
|
||||
wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
|
||||
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
|
||||
@@ -963,6 +987,7 @@ void WiFiComponent::check_scanning_finished() {
|
||||
return;
|
||||
}
|
||||
this->scan_done_ = false;
|
||||
this->did_scan_this_cycle_ = true;
|
||||
|
||||
if (this->scan_result_.empty()) {
|
||||
ESP_LOGW(TAG, "No networks found");
|
||||
@@ -1229,9 +1254,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
// After restart, go back to explicit hidden if we went through it initially, otherwise scan
|
||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
||||
// After restart, go back to explicit hidden if we went through it initially
|
||||
if (this->went_through_explicit_hidden_phase_()) {
|
||||
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
}
|
||||
// Skip scanning when captive portal/improv is active to avoid disrupting AP
|
||||
// Even passive scans can cause brief AP disconnections on ESP32
|
||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
|
||||
// Should never reach here
|
||||
@@ -1319,6 +1351,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||
this->restart_adapter();
|
||||
}
|
||||
// Clear scan flag - we're starting a new retry cycle
|
||||
this->did_scan_this_cycle_ = false;
|
||||
// Always enter cooldown after restart (or skip-restart) to allow stabilization
|
||||
// Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
||||
return true;
|
||||
|
||||
@@ -1414,10 +1452,8 @@ 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,
|
||||
old_priority, new_priority);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(),
|
||||
format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
|
||||
|
||||
// After adjusting priority, check if all priorities are now at minimum
|
||||
// If so, clear the vector to save memory and reset for fresh start
|
||||
|
||||
@@ -291,6 +291,7 @@ class WiFiComponent : public Component {
|
||||
void set_passive_scan(bool passive);
|
||||
|
||||
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
/// Setup WiFi interface.
|
||||
@@ -424,12 +425,14 @@ class WiFiComponent : public Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
void connect_soon_();
|
||||
|
||||
void wifi_loop_();
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
bool wifi_sta_pre_setup_();
|
||||
bool wifi_apply_output_power_(float output_power);
|
||||
bool wifi_apply_power_save_();
|
||||
bool wifi_sta_ip_config_(const optional<ManualIP> &manual_ip);
|
||||
bool wifi_sta_ip_config_(optional<ManualIP> manual_ip);
|
||||
bool wifi_apply_hostname_();
|
||||
bool wifi_sta_connect_(const WiFiAP &ap);
|
||||
void wifi_pre_setup_();
|
||||
@@ -437,7 +440,7 @@ class WiFiComponent : public Component {
|
||||
bool wifi_scan_start_(bool passive);
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool wifi_ap_ip_config_(const optional<ManualIP> &manual_ip);
|
||||
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
|
||||
bool wifi_start_ap_(const WiFiAP &ap);
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
@@ -529,6 +532,8 @@ class WiFiComponent : public Component {
|
||||
bool enable_on_boot_;
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
bool did_scan_this_cycle_{false};
|
||||
bool skip_cooldown_next_cycle_{false};
|
||||
|
||||
// Pointers at the end (naturally aligned)
|
||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||
|
||||
@@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t
|
||||
};
|
||||
#endif
|
||||
|
||||
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
@@ -525,10 +525,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
||||
s_sta_connect_not_found = true;
|
||||
} else {
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(it.bssid, bssid_s);
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
|
||||
LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
||||
format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
|
||||
s_sta_connect_error = true;
|
||||
}
|
||||
s_sta_connected = false;
|
||||
@@ -732,7 +730,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
@@ -487,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
@@ -746,10 +746,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf);
|
||||
return;
|
||||
} else {
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(it.bssid, bssid_s);
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
|
||||
get_disconnect_reason_str(it.reason));
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
||||
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
|
||||
s_sta_connect_error = true;
|
||||
}
|
||||
s_sta_connected = false;
|
||||
@@ -886,7 +884,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
esp_err_t err;
|
||||
|
||||
// enable AP
|
||||
|
||||
@@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() {
|
||||
return true;
|
||||
}
|
||||
bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); }
|
||||
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
@@ -299,10 +299,8 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
||||
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
||||
} else {
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(it.bssid, bssid_s);
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
|
||||
get_disconnect_reason_str(it.reason));
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
||||
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
|
||||
}
|
||||
|
||||
uint8_t reason = it.reason;
|
||||
@@ -436,7 +434,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
@@ -72,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
|
||||
bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
|
||||
|
||||
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
if (!manual_ip.has_value()) {
|
||||
return true;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
esphome::network::IPAddress ip_address, gateway, subnet, dns;
|
||||
if (manual_ip.has_value()) {
|
||||
ip_address = manual_ip->static_ip;
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.12.0-dev"
|
||||
__version__ = "2025.11.0b4"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
@@ -336,6 +336,7 @@ CONF_ENERGY = "energy"
|
||||
CONF_ENTITY_CATEGORY = "entity_category"
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ENUM_DATAPOINT = "enum_datapoint"
|
||||
CONF_ENVIRONMENT_VARIABLES = "environment_variables"
|
||||
CONF_EQUATION = "equation"
|
||||
CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
|
||||
CONF_ESPHOME = "esphome"
|
||||
|
||||
@@ -17,6 +17,7 @@ from esphome.const import (
|
||||
CONF_COMPILE_PROCESS_LIMIT,
|
||||
CONF_DEBUG_SCHEDULER,
|
||||
CONF_DEVICES,
|
||||
CONF_ENVIRONMENT_VARIABLES,
|
||||
CONF_ESPHOME,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_ID,
|
||||
@@ -215,6 +216,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.string_strict: cv.Any([cv.string], cv.string),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema(
|
||||
{
|
||||
cv.string_strict: cv.string,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_BOOT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger),
|
||||
@@ -426,6 +432,12 @@ async def _add_platformio_options(pio_options):
|
||||
cg.add_platformio_option(key, val)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_environment_variables(env_vars: dict[str, str]) -> None:
|
||||
# Set environment variables for the build process
|
||||
os.environ.update(env_vars)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _add_automations(config):
|
||||
for conf in config.get(CONF_ON_BOOT, []):
|
||||
@@ -563,6 +575,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_PLATFORMIO_OPTIONS]:
|
||||
CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS])
|
||||
|
||||
if config[CONF_ENVIRONMENT_VARIABLES]:
|
||||
CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES])
|
||||
|
||||
# Process areas
|
||||
all_areas: list[dict[str, str | core.ID]] = []
|
||||
if CONF_AREA in config:
|
||||
|
||||
@@ -111,23 +111,6 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n);
|
||||
/// @name Container utilities
|
||||
///@{
|
||||
|
||||
/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory)
|
||||
/// Avoids copying data from flash to RAM by keeping a pointer to the flash data.
|
||||
/// Similar to std::span but with minimal overhead for embedded systems.
|
||||
|
||||
template<typename T> class ConstVector {
|
||||
public:
|
||||
constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {}
|
||||
|
||||
const constexpr T &operator[](size_t i) const { return data_[i]; }
|
||||
constexpr size_t size() const { return size_; }
|
||||
constexpr bool empty() const { return size_ == 0; }
|
||||
|
||||
protected:
|
||||
const T *data_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/// Minimal static vector - saves memory by avoiding std::vector overhead
|
||||
template<typename T, size_t N> class StaticVector {
|
||||
public:
|
||||
|
||||
@@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
|
||||
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) ||
|
||||
has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) {
|
||||
(has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) ||
|
||||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) {
|
||||
// Skip scheduling - the retry was cancelled
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr);
|
||||
@@ -556,7 +556,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Mark items in defer queue as cancelled (they'll be skipped when processed)
|
||||
if (type == SchedulerItem::TIMEOUT) {
|
||||
total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry);
|
||||
total_cancelled +=
|
||||
this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry);
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
@@ -565,19 +566,20 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
// (removing the last element doesn't break heap structure)
|
||||
if (!this->items_.empty()) {
|
||||
auto &last_item = this->items_.back();
|
||||
if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) {
|
||||
if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) {
|
||||
this->recycle_item_(std::move(this->items_.back()));
|
||||
this->items_.pop_back();
|
||||
total_cancelled++;
|
||||
}
|
||||
// For other items in heap, we can only mark for removal (can't remove from middle of heap)
|
||||
size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry);
|
||||
size_t heap_cancelled =
|
||||
this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry);
|
||||
total_cancelled += heap_cancelled;
|
||||
this->to_remove_ += heap_cancelled; // Track removals for heap items
|
||||
}
|
||||
|
||||
// Cancel items in to_add_
|
||||
total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry);
|
||||
total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry);
|
||||
|
||||
return total_cancelled > 0;
|
||||
}
|
||||
|
||||
@@ -243,8 +243,18 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Helper function to check if item matches criteria for cancellation
|
||||
inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
inline bool HOT matches_item_locked_(const std::unique_ptr<SchedulerItem> &item, Component *component,
|
||||
const char *name_cstr, SchedulerItem::Type type, bool match_retry,
|
||||
bool skip_removed = true) const {
|
||||
// THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded
|
||||
// platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries.
|
||||
// PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and
|
||||
// has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper
|
||||
// functions should be safe regardless of caller behavior.
|
||||
// Fixes: https://github.com/esphome/esphome/issues/11940
|
||||
if (!item)
|
||||
return false;
|
||||
if (item->component != component || item->type != type || (skip_removed && item->remove) ||
|
||||
(match_retry && !item->is_retry)) {
|
||||
return false;
|
||||
@@ -304,8 +314,8 @@ class Scheduler {
|
||||
// SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_.
|
||||
// This is intentional and safe because:
|
||||
// 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_
|
||||
// and has_cancelled_timeout_in_container_ in scheduler.h)
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_
|
||||
// and has_cancelled_timeout_in_container_locked_ in scheduler.h)
|
||||
// 3. The lock protects concurrent access, but the nullptr remains until cleanup
|
||||
item = std::move(this->defer_queue_[this->defer_queue_front_]);
|
||||
this->defer_queue_front_++;
|
||||
@@ -393,10 +403,10 @@ class Scheduler {
|
||||
|
||||
// Helper to mark matching items in a container as removed
|
||||
// Returns the number of items marked for removal
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
template<typename Container>
|
||||
size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry) {
|
||||
size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry) {
|
||||
size_t count = 0;
|
||||
for (auto &item : container) {
|
||||
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
|
||||
@@ -405,7 +415,7 @@ class Scheduler {
|
||||
// the vector can still contain nullptr items from the processing loop. This check prevents crashes.
|
||||
if (!item)
|
||||
continue;
|
||||
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
|
||||
if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) {
|
||||
// Mark item for removal (platform-specific)
|
||||
this->set_item_removed_(item.get(), true);
|
||||
count++;
|
||||
@@ -415,9 +425,10 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Template helper to check if any item in a container matches our criteria
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
template<typename Container>
|
||||
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
|
||||
bool match_retry) const {
|
||||
bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component,
|
||||
const char *name_cstr, bool match_retry) const {
|
||||
for (const auto &item : container) {
|
||||
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
|
||||
// The defer_queue_ uses index-based processing: items are std::moved out but left in the
|
||||
@@ -426,8 +437,8 @@ class Scheduler {
|
||||
if (!item)
|
||||
continue;
|
||||
if (is_item_removed_(item.get()) &&
|
||||
this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
|
||||
/* skip_removed= */ false)) {
|
||||
this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
|
||||
/* skip_removed= */ false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pylint==4.0.3
|
||||
pylint==4.0.2
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.5 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.4 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==9.0.1
|
||||
pytest==9.0.0
|
||||
pytest-cov==7.0.0
|
||||
pytest-mock==3.15.1
|
||||
pytest-asyncio==1.3.0
|
||||
|
||||
@@ -2,6 +2,9 @@ esphome:
|
||||
debug_scheduler: true
|
||||
platformio_options:
|
||||
board_build.flash_mode: dio
|
||||
environment_variables:
|
||||
TEST_ENV_VAR: "test_value"
|
||||
BUILD_NUMBER: "12345"
|
||||
area:
|
||||
id: testing_area
|
||||
name: Testing Area
|
||||
|
||||
@@ -76,7 +76,7 @@ lvgl:
|
||||
line_width: 8
|
||||
line_rounded: true
|
||||
- id: date_style
|
||||
text_font: !lambda return id(roboto10);
|
||||
text_font: roboto10
|
||||
align: center
|
||||
text_color: !lambda return color_id2;
|
||||
bg_opa: cover
|
||||
@@ -267,7 +267,7 @@ lvgl:
|
||||
snprintf(buf, sizeof(buf), "Setup: %d", 42);
|
||||
return std::string(buf);
|
||||
align: top_mid
|
||||
text_font: !lambda return id(space16);
|
||||
text_font: space16
|
||||
- label:
|
||||
id: chip_info_label
|
||||
# Test complex setup lambda (real-world pattern)
|
||||
@@ -703,7 +703,9 @@ lvgl:
|
||||
on_value:
|
||||
- lvgl.spinbox.update:
|
||||
id: spinbox_id
|
||||
value: !lambda return x;
|
||||
value: !lambda |-
|
||||
static float yyy = 83.0;
|
||||
return yyy + .8;
|
||||
- button:
|
||||
styles: spin_button
|
||||
id: spin_up
|
||||
|
||||
@@ -18,7 +18,6 @@ touchscreen:
|
||||
|
||||
lvgl:
|
||||
- id: lvgl_0
|
||||
default_font: space16
|
||||
displays: sdl0
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
@@ -40,8 +39,3 @@ lvgl:
|
||||
text: Click ME
|
||||
on_click:
|
||||
logger.log: Clicked
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: space16
|
||||
bpp: 4
|
||||
|
||||
@@ -15,7 +15,6 @@ nrf52:
|
||||
inverted: true
|
||||
mode:
|
||||
output: true
|
||||
dcdc: False
|
||||
reg0:
|
||||
voltage: 2.1V
|
||||
uicr_erase: true
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
esphome:
|
||||
name: test-timeout-filters
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Template sensors that we'll use to publish values
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Source Timeout Last"
|
||||
id: source_timeout_last
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Reset"
|
||||
id: source_timeout_reset
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Static"
|
||||
id: source_timeout_static
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Lambda"
|
||||
id: source_timeout_lambda
|
||||
accuracy_decimals: 1
|
||||
|
||||
# Test 1: TimeoutFilter - "last" mode (outputs last received value)
|
||||
- platform: copy
|
||||
source_id: source_timeout_last
|
||||
name: "Timeout Last Sensor"
|
||||
id: timeout_last_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: last # Explicitly specify "last" mode to use TimeoutFilter class
|
||||
|
||||
# Test 2: TimeoutFilter - reset behavior (same filter, different source)
|
||||
- platform: copy
|
||||
source_id: source_timeout_reset
|
||||
name: "Timeout Reset Sensor"
|
||||
id: timeout_reset_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: last # Explicitly specify "last" mode
|
||||
|
||||
# Test 3: TimeoutFilterConfigured - static value mode
|
||||
- platform: copy
|
||||
source_id: source_timeout_static
|
||||
name: "Timeout Static Sensor"
|
||||
id: timeout_static_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: 99.9
|
||||
|
||||
# Test 4: TimeoutFilterConfigured - lambda mode
|
||||
- platform: copy
|
||||
source_id: source_timeout_lambda
|
||||
name: "Timeout Lambda Sensor"
|
||||
id: timeout_lambda_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: !lambda "return -1.0;"
|
||||
|
||||
# Scripts to publish values with controlled timing
|
||||
script:
|
||||
# Test 1: Single value followed by timeout
|
||||
- id: test_timeout_last_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_last
|
||||
state: 42.0
|
||||
# Wait for timeout to fire (100ms + margin)
|
||||
- delay: 150ms
|
||||
|
||||
# Test 2: Multiple values before timeout (should reset timer)
|
||||
- id: test_timeout_reset_script
|
||||
then:
|
||||
# Publish first value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 10.0
|
||||
# Wait 50ms (halfway to timeout)
|
||||
- delay: 50ms
|
||||
# Publish second value (resets timeout)
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 20.0
|
||||
# Wait 50ms (halfway to timeout again)
|
||||
- delay: 50ms
|
||||
# Publish third value (resets timeout)
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 30.0
|
||||
# Wait for timeout to fire (100ms + margin)
|
||||
- delay: 150ms
|
||||
|
||||
# Test 3: Static value timeout
|
||||
- id: test_timeout_static_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_static
|
||||
state: 55.5
|
||||
# Wait for timeout to fire
|
||||
- delay: 150ms
|
||||
|
||||
# Test 4: Lambda value timeout
|
||||
- id: test_timeout_lambda_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_lambda
|
||||
state: 77.7
|
||||
# Wait for timeout to fire
|
||||
- delay: 150ms
|
||||
|
||||
# Buttons to trigger each test scenario
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Timeout Last Button"
|
||||
id: test_timeout_last_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_last_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Reset Button"
|
||||
id: test_timeout_reset_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_reset_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Static Button"
|
||||
id: test_timeout_static_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_static_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Lambda Button"
|
||||
id: test_timeout_lambda_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_lambda_script
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Test sensor timeout filter functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_timeout_filter(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test TimeoutFilter and TimeoutFilterConfigured with all modes."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track state changes for all sensors
|
||||
timeout_last_states: list[float] = []
|
||||
timeout_reset_states: list[float] = []
|
||||
timeout_static_states: list[float] = []
|
||||
timeout_lambda_states: list[float] = []
|
||||
|
||||
# Futures for each test scenario
|
||||
test1_complete = loop.create_future() # TimeoutFilter - last mode
|
||||
test2_complete = loop.create_future() # TimeoutFilter - reset behavior
|
||||
test3_complete = loop.create_future() # TimeoutFilterConfigured - static value
|
||||
test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
"""Track sensor state updates."""
|
||||
if not isinstance(state, SensorState):
|
||||
return
|
||||
|
||||
if state.missing_state:
|
||||
return
|
||||
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
|
||||
# Test 1: TimeoutFilter - last mode
|
||||
if sensor_name == "timeout_last_sensor":
|
||||
timeout_last_states.append(state.state)
|
||||
# Expect 2 values: initial 42.0 + timeout fires with 42.0
|
||||
if len(timeout_last_states) >= 2 and not test1_complete.done():
|
||||
test1_complete.set_result(True)
|
||||
|
||||
# Test 2: TimeoutFilter - reset behavior
|
||||
elif sensor_name == "timeout_reset_sensor":
|
||||
timeout_reset_states.append(state.state)
|
||||
# Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0
|
||||
if len(timeout_reset_states) >= 4 and not test2_complete.done():
|
||||
test2_complete.set_result(True)
|
||||
|
||||
# Test 3: TimeoutFilterConfigured - static value
|
||||
elif sensor_name == "timeout_static_sensor":
|
||||
timeout_static_states.append(state.state)
|
||||
# Expect 2 values: initial 55.5 + timeout fires with 99.9
|
||||
if len(timeout_static_states) >= 2 and not test3_complete.done():
|
||||
test3_complete.set_result(True)
|
||||
|
||||
# Test 4: TimeoutFilterConfigured - lambda
|
||||
elif sensor_name == "timeout_lambda_sensor":
|
||||
timeout_lambda_states.append(state.state)
|
||||
# Expect 2 values: initial 77.7 + timeout fires with -1.0
|
||||
if len(timeout_lambda_states) >= 2 and not test4_complete.done():
|
||||
test4_complete.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, services = await client.list_entities_services()
|
||||
|
||||
key_to_sensor = build_key_to_entity_mapping(
|
||||
entities,
|
||||
[
|
||||
"timeout_last_sensor",
|
||||
"timeout_reset_sensor",
|
||||
"timeout_static_sensor",
|
||||
"timeout_lambda_sensor",
|
||||
],
|
||||
)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Helper to find buttons by object_id substring
|
||||
def find_button(object_id_substring: str) -> int:
|
||||
"""Find a button by object_id substring and return its key."""
|
||||
button = next(
|
||||
(e for e in entities if object_id_substring in e.object_id.lower()),
|
||||
None,
|
||||
)
|
||||
assert button is not None, f"Button '{object_id_substring}' not found"
|
||||
return button.key
|
||||
|
||||
# Find all test buttons
|
||||
test1_button_key = find_button("test_timeout_last_button")
|
||||
test2_button_key = find_button("test_timeout_reset_button")
|
||||
test3_button_key = find_button("test_timeout_static_button")
|
||||
test4_button_key = find_button("test_timeout_lambda_button")
|
||||
|
||||
# === Test 1: TimeoutFilter - last mode ===
|
||||
client.button_command(test1_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test1_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}")
|
||||
|
||||
assert len(timeout_last_states) == 2, (
|
||||
f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}"
|
||||
)
|
||||
assert timeout_last_states[0] == pytest.approx(42.0), (
|
||||
f"Test 1: First state should be 42.0, got {timeout_last_states[0]}"
|
||||
)
|
||||
assert timeout_last_states[1] == pytest.approx(42.0), (
|
||||
f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}"
|
||||
)
|
||||
|
||||
# === Test 2: TimeoutFilter - reset behavior ===
|
||||
client.button_command(test2_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}")
|
||||
|
||||
assert len(timeout_reset_states) == 4, (
|
||||
f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}"
|
||||
)
|
||||
assert timeout_reset_states[0] == pytest.approx(10.0), (
|
||||
f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}"
|
||||
)
|
||||
assert timeout_reset_states[1] == pytest.approx(20.0), (
|
||||
f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}"
|
||||
)
|
||||
assert timeout_reset_states[2] == pytest.approx(30.0), (
|
||||
f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}"
|
||||
)
|
||||
assert timeout_reset_states[3] == pytest.approx(30.0), (
|
||||
f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}"
|
||||
)
|
||||
|
||||
# === Test 3: TimeoutFilterConfigured - static value ===
|
||||
client.button_command(test3_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test3_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}")
|
||||
|
||||
assert len(timeout_static_states) == 2, (
|
||||
f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}"
|
||||
)
|
||||
assert timeout_static_states[0] == pytest.approx(55.5), (
|
||||
f"Test 3: First state should be 55.5, got {timeout_static_states[0]}"
|
||||
)
|
||||
assert timeout_static_states[1] == pytest.approx(99.9), (
|
||||
f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}"
|
||||
)
|
||||
|
||||
# === Test 4: TimeoutFilterConfigured - lambda ===
|
||||
client.button_command(test4_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test4_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}")
|
||||
|
||||
assert len(timeout_lambda_states) == 2, (
|
||||
f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}"
|
||||
)
|
||||
assert timeout_lambda_states[0] == pytest.approx(77.7), (
|
||||
f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}"
|
||||
)
|
||||
assert timeout_lambda_states[1] == pytest.approx(-1.0), (
|
||||
f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}"
|
||||
)
|
||||
Reference in New Issue
Block a user