Merge remote-tracking branch 'upstream/dev' into 20250915-wifi-info-use-callbacks

This commit is contained in:
kbx81
2025-11-24 15:45:28 -06:00
99 changed files with 1659 additions and 260 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>
+1
View File
@@ -484,6 +484,7 @@ esphome/components/template/datetime/* @rfdarter
esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermopro_ble/* @sittner
esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
+1 -1
View File
@@ -1319,7 +1319,7 @@ def parse_args(argv):
"clean-all", help="Clean all build and platform files."
)
parser_clean_all.add_argument(
"configuration", help="Your YAML configuration directory.", nargs="*"
"configuration", help="Your YAML file or configuration directory.", nargs="*"
)
parser_dashboard = subparsers.add_parser(
@@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() {
break;
default:
this->publish_state(NAN);
this->status_set_error("Invalid saturation vapor pressure equation selection!");
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
return;
}
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
+1 -1
View File
@@ -83,7 +83,7 @@ void AHT10Component::setup() {
void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0;
this->status_set_error("Reading timed out");
this->status_set_error(LOG_STR("Reading timed out"));
return;
}
this->read_count_++;
+18 -2
View File
@@ -85,6 +85,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
def validate_encryption_key(value):
@@ -537,9 +538,24 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
return var
@automation.register_condition("api.connected", APIConnectedCondition, {})
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
cv.boolean
),
}
)
@automation.register_condition(
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
)
async def api_connected_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
var = cg.new_Pvariable(condition_id, template_arg)
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
cg.add(var.set_state_subscription_only(templ))
return var
def FILTER_SOURCE_FILES() -> list[str]:
+10 -6
View File
@@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000;
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) {
auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) {
this->helper_ =
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else {
@@ -1451,8 +1451,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_AREAS
resp.set_suggested_area(StringRef(App.get_area()));
#endif
// mac_address must store temporary string - will be valid during send_message call
std::string mac_address = get_mac_address_pretty();
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
char mac_address[18];
uint8_t mac[6];
get_mac_address_raw(mac);
format_mac_addr_upper(mac, mac_address);
resp.set_mac_address(StringRef(mac_address));
resp.set_esphome_version(ESPHOME_VERSION_REF);
@@ -1493,8 +1496,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#endif
#ifdef USE_BLUETOOTH_PROXY
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
// bt_mac must store temporary string - will be valid during send_message call
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
char bluetooth_mac[18];
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
#endif
#ifdef USE_VOICE_ASSISTANT
+3 -6
View File
@@ -84,9 +84,7 @@ class APIFrameHelper {
public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_owned_(std::move(socket)), client_info_(client_info) {
socket_ = socket_owned_.get();
}
: socket_(std::move(socket)), client_info_(client_info) {}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
@@ -149,9 +147,8 @@ class APIFrameHelper {
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Pointers first (4 bytes each)
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
@@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::SERVER_HELLO) {
// send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
char mac[mac_len];
get_mac_address_into_buffer(mac);
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
@@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() {
// node name, terminated by null byte
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
std::memcpy(msg.get() + mac_offset, mac, mac_len);
aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK)
@@ -527,7 +528,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
const auto &psk = ctx_->get_psk();
const auto &psk = this->ctx_.get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -9,9 +9,8 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
+15 -4
View File
@@ -227,8 +227,8 @@ void APIServer::dump_config() {
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
}
#else
@@ -493,7 +493,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGW(TAG, "Key set in YAML");
return false;
#else
auto &old_psk = this->noise_ctx_->get_psk();
auto &old_psk = this->noise_ctx_.get_psk();
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
ESP_LOGW(TAG, "New PSK matches old");
return true;
@@ -528,7 +528,18 @@ void APIServer::request_time() {
}
#endif
bool APIServer::is_connected() const { return !this->clients_.empty(); }
bool APIServer::is_connected(bool state_subscription_only) const {
if (!state_subscription_only) {
return !this->clients_.empty();
}
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
}
}
return false;
}
void APIServer::on_shutdown() {
this->shutting_down_ = true;
+8 -5
View File
@@ -54,8 +54,8 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
@@ -150,7 +150,7 @@ class APIServer : public Component, public Controller {
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
#endif
bool is_connected() const;
bool is_connected(bool state_subscription_only = false) const;
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
@@ -228,7 +228,7 @@ class APIServer : public Component, public Controller {
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
APINoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
};
@@ -236,8 +236,11 @@ class APIServer : public Component, public Controller {
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public:
bool check(const Ts &...x) override { return global_api_server->is_connected(); }
bool check(const Ts &...x) override {
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
}
};
} // namespace esphome::api
+1 -1
View File
@@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() {
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
}
@@ -130,11 +130,13 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
return flags;
}
std::string get_bluetooth_mac_address_pretty() {
void get_bluetooth_mac_address_pretty(std::span<char, 18> output) {
const uint8_t *mac = esp_bt_dev_get_address();
char buf[18];
format_mac_addr_upper(mac, buf);
return std::string(buf);
if (mac != nullptr) {
format_mac_addr_upper(mac, output.data());
} else {
output[0] = '\0';
}
}
protected:
@@ -100,18 +100,18 @@ void BME280Component::setup() {
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (chip_id != 0x60) {
this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID));
return;
}
// Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed("Reset failed");
this->mark_failed(LOG_STR("Reset failed"));
return;
}
// Wait until the NVM data has finished loading.
@@ -120,12 +120,12 @@ void BME280Component::setup() {
do { // NOLINT
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
this->mark_failed("Error reading status register");
this->mark_failed(LOG_STR("Error reading status register"));
return;
}
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) {
this->mark_failed("Timeout loading NVM");
this->mark_failed(LOG_STR("Timeout loading NVM"));
return;
}
@@ -153,26 +153,26 @@ void BME280Component::setup() {
uint8_t humid_control_val = 0;
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
this->mark_failed("Read humidity control");
this->mark_failed(LOG_STR("Read humidity control"));
return;
}
humid_control_val &= ~0b00000111;
humid_control_val |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
this->mark_failed("Write humidity control");
this->mark_failed(LOG_STR("Write humidity control"));
return;
}
uint8_t config_register = 0;
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
this->mark_failed("Read config");
this->mark_failed(LOG_STR("Read config"));
return;
}
config_register &= ~0b11111100;
config_register |= 0b101 << 5; // 1000 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
this->mark_failed("Write config");
this->mark_failed(LOG_STR("Write config"));
return;
}
}
@@ -65,23 +65,23 @@ void BMP280Component::setup() {
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (chip_id != 0x58) {
this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID));
return;
}
// Send a soft reset.
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
this->mark_failed("Reset failed");
this->mark_failed(LOG_STR("Reset failed"));
return;
}
// Wait until the NVM data has finished loading.
@@ -90,12 +90,12 @@ void BMP280Component::setup() {
do {
delay(2);
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
this->mark_failed("Error reading status register");
this->mark_failed(LOG_STR("Error reading status register"));
return;
}
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
if (status & BMP280_STATUS_IM_UPDATE) {
this->mark_failed("Timeout loading NVM");
this->mark_failed(LOG_STR("Timeout loading NVM"));
return;
}
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
uint8_t config_register = 0;
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
this->mark_failed("Read config");
this->mark_failed(LOG_STR("Read config"));
return;
}
config_register &= ~0b11111100;
config_register |= 0b000 << 5; // 0.5 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
this->mark_failed("Write config");
this->mark_failed(LOG_STR("Write config"));
return;
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr;
Camera::Camera() {
if (global_camera != nullptr) {
this->status_set_error("Multiple cameras are configured, but only one is supported.");
this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported."));
this->mark_failed();
return;
}
@@ -20,13 +20,13 @@ void CST816Touchscreen::continue_setup_() {
break;
default:
ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_);
this->status_set_error("Unknown chip ID");
this->status_set_error(LOG_STR("Unknown chip ID"));
this->mark_failed();
return;
}
this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION);
} else if (!this->skip_probe_) {
this->status_set_error("Failed to read chip id");
this->status_set_error(LOG_STR("Failed to read chip id"));
this->mark_failed();
return;
}
+2 -2
View File
@@ -22,7 +22,7 @@ const char *EPaperBase::epaper_state_to_string_() {
void EPaperBase::setup() {
if (!this->init_buffer_(this->buffer_length_)) {
this->mark_failed("Failed to initialise buffer");
this->mark_failed(LOG_STR("Failed to initialise buffer"));
return;
}
this->setup_pins_();
@@ -246,7 +246,7 @@ void EPaperBase::initialise_() {
auto length = this->init_sequence_length_;
while (index != length) {
if (length - index < 2) {
this->mark_failed("Malformed init sequence");
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
const uint8_t cmd = sequence[index++];
@@ -88,7 +88,7 @@ void Esp32HostedUpdate::perform(bool force) {
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
this->status_set_error("SHA256 verification failed");
this->status_set_error(LOG_STR("SHA256 verification failed"));
this->publish_state();
return;
}
@@ -105,7 +105,7 @@ void Esp32HostedUpdate::perform(bool force) {
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to begin OTA");
this->status_set_error(LOG_STR("Failed to begin OTA"));
this->publish_state();
return;
}
@@ -121,7 +121,7 @@ void Esp32HostedUpdate::perform(bool force) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
this->state_ = prev_state;
this->status_set_error("Failed to write OTA data");
this->status_set_error(LOG_STR("Failed to write OTA data"));
this->publish_state();
return;
}
@@ -134,7 +134,7 @@ void Esp32HostedUpdate::perform(bool force) {
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to end OTA");
this->status_set_error(LOG_STR("Failed to end OTA"));
this->publish_state();
return;
}
@@ -144,7 +144,7 @@ void Esp32HostedUpdate::perform(bool force) {
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to activate OTA");
this->status_set_error(LOG_STR("Failed to activate OTA"));
this->publish_state();
return;
}
+2 -2
View File
@@ -14,8 +14,8 @@ void EspLdo::setup() {
config.flags.adjustable = this->adjustable_;
auto err = esp_ldo_acquire_channel(&config, &this->handle_);
if (err != ESP_OK) {
auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
this->mark_failed(msg.c_str());
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
this->mark_failed(LOG_STR("Failed to acquire LDO channel"));
} else {
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_);
}
+1
View File
@@ -383,6 +383,7 @@ async def to_code(config):
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code
@@ -553,11 +553,14 @@ void EthernetComponent::start_connect_() {
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else {
} else
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
@@ -578,6 +581,7 @@ void EthernetComponent::start_connect_() {
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
@@ -590,7 +594,9 @@ void EthernetComponent::start_connect_() {
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else {
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
@@ -688,7 +694,9 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
@@ -82,7 +82,9 @@ class EthernetComponent : public Component {
void add_phy_register(PHYRegister register_value);
#endif
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
@@ -137,7 +139,9 @@ class EthernetComponent : public Component {
uint8_t mdc_pin_{23};
uint8_t mdio_pin_{18};
#endif
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
uint32_t connect_begin_;
// Group all uint8_t types together (enums and bools)
+3 -3
View File
@@ -36,20 +36,20 @@ void GDK101Component::setup() {
uint8_t data[2];
// first, reset the sensor
if (!this->reset_sensor_(data)) {
this->status_set_error("Reset failed!");
this->status_set_error(LOG_STR("Reset failed!"));
this->mark_failed();
return;
}
// sensor should acknowledge success of the reset procedure
if (data[0] != 1) {
this->status_set_error("Reset not acknowledged!");
this->status_set_error(LOG_STR("Reset not acknowledged!"));
this->mark_failed();
return;
}
delay(10);
// read firmware version
if (!this->read_fw_version_(data)) {
this->status_set_error("Failed to read firmware version");
this->status_set_error(LOG_STR("Failed to read firmware version"));
this->mark_failed();
return;
}
@@ -79,13 +79,13 @@ void GT911Touchscreen::setup_internal_() {
}
}
if (err != i2c::ERROR_OK) {
this->mark_failed("Calibration error");
this->mark_failed(LOG_STR("Calibration error"));
return;
}
}
if (err != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->setup_done_ = true;
@@ -29,7 +29,7 @@ void HttpRequestUpdate::setup() {
this->publish_state();
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error("Failed to install firmware");
this->status_set_error(LOG_STR("Failed to install firmware"));
this->publish_state();
}
});
@@ -51,7 +51,7 @@ void HttpRequestUpdate::update_task(void *params) {
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); });
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
UPDATE_RETURN;
}
@@ -60,7 +60,8 @@ void HttpRequestUpdate::update_task(void *params) {
if (data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); });
this_update->defer(
[this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
container->end();
UPDATE_RETURN;
}
@@ -123,7 +124,7 @@ void HttpRequestUpdate::update_task(void *params) {
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); });
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
UPDATE_RETURN;
}
+15 -11
View File
@@ -47,18 +47,20 @@ MULTI_CONF = True
def _bus_declare_type(value):
if CORE.is_esp32:
return cv.declare_id(IDFI2CBus)(value)
if CORE.using_arduino:
return cv.declare_id(ArduinoI2CBus)(value)
if CORE.using_esp_idf:
return cv.declare_id(IDFI2CBus)(value)
if CORE.using_zephyr:
return cv.declare_id(ZephyrI2CBus)(value)
raise NotImplementedError
def validate_config(config):
if CORE.using_esp_idf:
return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config)
if CORE.is_esp32:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
)(config)
return config
@@ -67,12 +69,12 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): _bus_declare_type,
cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
cv.SplitDefault(
CONF_FREQUENCY,
@@ -151,7 +153,7 @@ async def to_code(config):
cg.add(var.set_scan(config[CONF_SCAN]))
if CONF_TIMEOUT in config:
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
if CORE.using_arduino:
if CORE.using_arduino and not CORE.is_esp32:
cg.add_library("Wire", None)
@@ -248,14 +250,16 @@ def final_validate_device_schema(
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"i2c_bus_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"i2c_bus_esp_idf.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
}
)
+5 -19
View File
@@ -1,4 +1,4 @@
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#include "i2c_bus_arduino.h"
#include <Arduino.h>
@@ -15,16 +15,7 @@ static const char *const TAG = "i2c.arduino";
void ArduinoI2CBus::setup() {
recover_();
#if defined(USE_ESP32)
static uint8_t next_bus_num = 0;
if (next_bus_num == 0) {
wire_ = &Wire;
} else {
wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory)
}
this->port_ = next_bus_num;
next_bus_num++;
#elif defined(USE_ESP8266)
#if defined(USE_ESP8266)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
#elif defined(USE_RP2040)
static bool first = true;
@@ -54,10 +45,7 @@ void ArduinoI2CBus::set_pins_and_clock_() {
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
#endif
if (timeout_ > 0) { // if timeout specified in yaml
#if defined(USE_ESP32)
// https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp
wire_->setTimeOut(timeout_ / 1000); // unit: ms
#elif defined(USE_ESP8266)
#if defined(USE_ESP8266)
// https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h
wire_->setClockStretchLimit(timeout_); // unit: us
#elif defined(USE_RP2040)
@@ -76,9 +64,7 @@ void ArduinoI2CBus::dump_config() {
" Frequency: %u Hz",
this->sda_pin_, this->scl_pin_, this->frequency_);
if (timeout_ > 0) {
#if defined(USE_ESP32)
ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000);
#elif defined(USE_ESP8266)
#if defined(USE_ESP8266)
ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_);
#elif defined(USE_RP2040)
ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000);
@@ -275,4 +261,4 @@ void ArduinoI2CBus::recover_() {
} // namespace i2c
} // namespace esphome
#endif // USE_ESP_IDF
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)
+3 -4
View File
@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#include <Wire.h>
#include "esphome/core/component.h"
@@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
int get_port() const override { return this->port_; }
int get_port() const override { return 0; }
private:
void recover_();
@@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
RecoveryCode recovery_result_;
protected:
int8_t port_{-1};
TwoWire *wire_;
uint8_t sda_pin_;
uint8_t scl_pin_;
@@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
} // namespace i2c
} // namespace esphome
#endif // USE_ARDUINO
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)
+2 -2
View File
@@ -1,4 +1,4 @@
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "i2c_bus_esp_idf.h"
@@ -299,4 +299,4 @@ void IDFI2CBus::recover_() {
} // namespace i2c
} // namespace esphome
#endif // USE_ESP_IDF
#endif // USE_ESP32
+2 -2
View File
@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "esphome/core/component.h"
#include "i2c_bus.h"
@@ -53,4 +53,4 @@ class IDFI2CBus : public InternalI2CBus, public Component {
} // namespace i2c
} // namespace esphome
#endif // USE_ESP_IDF
#endif // USE_ESP32
+24
View File
@@ -23,6 +23,9 @@ void LightState::setup() {
effect->init_internal(this);
}
// Start with loop disabled if idle - respects any effects/transitions set up during initialization
this->disable_loop_if_idle_();
// When supported color temperature range is known, initialize color temperature setting within bounds.
auto traits = this->get_traits();
float min_mireds = traits.get_min_mireds();
@@ -125,6 +128,9 @@ void LightState::loop() {
this->is_transformer_active_ = false;
this->transformer_ = nullptr;
this->target_state_reached_callback_.call();
// Disable loop if idle (no transformer and no effect)
this->disable_loop_if_idle_();
}
}
@@ -132,6 +138,8 @@ void LightState::loop() {
if (this->next_write_) {
this->next_write_ = false;
this->output_->write_state(this);
// Disable loop if idle (no transformer and no effect)
this->disable_loop_if_idle_();
}
}
@@ -227,6 +235,8 @@ void LightState::start_effect_(uint32_t effect_index) {
this->active_effect_index_ = effect_index;
auto *effect = this->get_active_effect_();
effect->start_internal();
// Enable loop while effect is active
this->enable_loop();
}
LightEffect *LightState::get_active_effect_() {
if (this->active_effect_index_ == 0) {
@@ -241,6 +251,8 @@ void LightState::stop_effect_() {
effect->stop();
}
this->active_effect_index_ = 0;
// Disable loop if idle (no effect and no transformer)
this->disable_loop_if_idle_();
}
void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
@@ -250,6 +262,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng
if (set_remote_values) {
this->remote_values = target;
}
// Enable loop while transition is active
this->enable_loop();
}
void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
@@ -265,6 +279,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
if (set_remote_values) {
this->remote_values = target;
};
// Enable loop while flash is active
this->enable_loop();
}
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
@@ -276,6 +292,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
}
this->output_->update_state(this);
this->next_write_ = true;
this->enable_loop();
}
void LightState::disable_loop_if_idle_() {
// Only disable loop if both transformer and effect are inactive, and no pending writes
if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) {
this->disable_loop();
}
}
void LightState::save_remote_values_() {
+3
View File
@@ -255,6 +255,9 @@ class LightState : public EntityBase, public Component {
/// Internal method to save the current remote_values to the preferences
void save_remote_values_();
/// Disable loop if neither transformer nor effect is active
void disable_loop_if_idle_();
/// Store the output to allow effects to have more access.
LightOutput *output_;
/// The currently active transformer for this light (transition/flash).
+2
View File
@@ -365,8 +365,10 @@ async def to_code(config):
if CORE.is_esp32:
if config[CONF_HARDWARE_UART] == USB_CDC:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
try:
uart_selection(USB_SERIAL_JTAG)
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
+11 -10
View File
@@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
uint16_t buffer_at = 0; // Initialize buffer position
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
MAX_CONSOLE_LOG_MSG_SIZE);
this->write_msg_(console_buffer);
// Add newline if platform needs it (ESP32 doesn't add via write_msg_)
this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
this->write_msg_(console_buffer, buffer_at);
}
// Reset the recursion guard for this task
@@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
// Save the offset before calling format_log_to_buffer_with_terminator_
// since it will increment tx_buffer_at_ to the end of the formatted string
uint32_t msg_start = this->tx_buffer_at_;
uint16_t msg_start = this->tx_buffer_at_;
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
&this->tx_buffer_at_, this->tx_buffer_size_);
// Write to console and send callback starting at the msg_start
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start);
}
size_t msg_length =
uint16_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
// Callbacks get message first (before console write)
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
// Write to console starting at the msg_start
this->write_tx_buffer_to_console_(msg_start, &msg_length);
global_recursion_guard_ = false;
}
#endif // USE_STORE_LOG_STR_IN_FLASH
@@ -209,9 +212,7 @@ void Logger::process_messages_() {
// This ensures all log messages appear on the console in a clean, serialized manner
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
}
this->write_tx_buffer_to_console_();
}
} else {
// No messages to process, disable loop if appropriate
+48 -5
View File
@@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Platform-specific: does write_msg_ add its own newline?
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
// Allows single write call with newline included for efficiency
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
// Newline should NOT be added to buffer
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
#else
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection
*
@@ -173,7 +184,7 @@ class Logger : public Component {
protected:
void process_messages_();
void write_msg_(const char *msg);
void write_msg_(const char *msg, size_t len);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// It's the caller's responsibility to initialize buffer_at (typically to 0)
@@ -200,6 +211,35 @@ class Logger : public Component {
}
}
// Helper to add newline to buffer for platforms that need it
// Modifies buffer_at to include the newline
inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
if constexpr (!WRITE_MSG_ADDS_NEWLINE) {
// Add newline - don't need to maintain null termination
// write_msg_ now always receives explicit length, so we can safely overwrite the null terminator
// This is safe because:
// 1. Callbacks already received the message (before we add newline)
// 2. write_msg_ receives the length explicitly (doesn't need null terminator)
if (*buffer_at < buffer_size) {
buffer[(*buffer_at)++] = '\n';
} else if (buffer_size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
buffer[buffer_size - 1] = '\n';
*buffer_at = buffer_size;
}
}
}
// Helper to write tx_buffer_ to console if logging is enabled
// INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null
inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) {
if (this->baud_rate_ > 0) {
uint16_t *len_ptr = length ? length : &this->tx_buffer_at_;
this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset);
this->write_msg_(this->tx_buffer_ + offset, *len_ptr);
}
}
// Helper to format and send a log message to both console and callbacks
inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format,
va_list args) {
@@ -208,10 +248,11 @@ class Logger : public Component {
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
this->tx_buffer_size_);
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
}
// Callbacks get message WITHOUT newline (for API/MQTT/syslog)
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
// Console gets message WITH newline (if platform needs it)
this->write_tx_buffer_to_console_();
}
// Write the body of the log message to the buffer
@@ -425,7 +466,9 @@ class Logger : public Component {
}
// Update buffer_at with the formatted length (handle truncation)
uint16_t formatted_len = (ret >= remaining) ? remaining : ret;
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
*buffer_at += formatted_len;
// Remove all trailing newlines right after formatting
+15 -17
View File
@@ -121,25 +121,23 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
if (
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Length is now always passed explicitly - no strlen() fallback needed
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
// USB CDC/JTAG - single write including newline (already in buffer)
// Use fwrite to stdout which goes through VFS to USB console
//
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
// to configure correctly for their hardware. This approach eliminates runtime overhead.
fwrite(msg, 1, len, stdout);
#else
/* DISABLES CODE */ (false) // NOLINT
// Regular UART - single write including newline (already in buffer)
uart_write_bytes(this->uart_num_, msg, len);
#endif
) {
puts(msg);
} else {
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
uart_write_bytes(this->uart_num_, msg, len);
uart_write_bytes(this->uart_num_, "\n", 1);
}
}
const LogString *Logger::get_uart_selection_() {
+4 -1
View File
@@ -33,7 +33,10 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Single write with newline already in buffer (added by caller)
this->hw_serial_->write(msg, len);
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
+1 -1
View File
@@ -3,7 +3,7 @@
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg) {
void HOT Logger::write_msg_(const char *msg, size_t) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
@@ -49,7 +49,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
+1 -1
View File
@@ -27,7 +27,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
+1 -1
View File
@@ -62,7 +62,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
void HOT Logger::write_msg_(const char *msg, size_t) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
+2 -2
View File
@@ -466,7 +466,7 @@ void LvglComponent::setup() {
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
}
if (buffer == nullptr) {
this->status_set_error("Memory allocation failure");
this->status_set_error(LOG_STR("Memory allocation failure"));
this->mark_failed();
return;
}
@@ -479,7 +479,7 @@ void LvglComponent::setup() {
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
if (this->rotate_buf_ == nullptr) {
this->status_set_error("Memory allocation failure");
this->status_set_error(LOG_STR("Memory allocation failure"));
this->mark_failed();
return;
}
+2 -2
View File
@@ -57,14 +57,14 @@ void MAX17043Component::setup() {
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
ESP_LOGE(TAG, "Device does not appear to be a MAX17043");
this->status_set_error("unrecognised");
this->status_set_error(LOG_STR("unrecognised"));
this->mark_failed();
return;
}
// need to write back to config register to reset the sleep bit
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
this->status_set_error("sleep reset failed");
this->status_set_error(LOG_STR("sleep reset failed"));
this->mark_failed();
return;
}
+1 -1
View File
@@ -118,7 +118,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
bool has_psk = api::global_api_server->get_noise_ctx().has_psk();
const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
#endif
+15 -9
View File
@@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel
xSemaphoreGiveFromISR(sem, &need_yield);
return (need_yield == pdTRUE);
}
void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) {
ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err));
this->mark_failed(message);
}
void MIPI_DSI::setup() {
ESP_LOGCONFIG(TAG, "Running Setup");
@@ -31,7 +37,7 @@ void MIPI_DSI::setup() {
};
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
if (err != ESP_OK) {
this->smark_failed("lcd_new_dsi_bus failed", err);
this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err);
return;
}
esp_lcd_dbi_io_config_t dbi_config = {
@@ -41,7 +47,7 @@ void MIPI_DSI::setup() {
};
err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_);
if (err != ESP_OK) {
this->smark_failed("new_panel_io_dbi failed", err);
this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err);
return;
}
auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565;
@@ -69,7 +75,7 @@ void MIPI_DSI::setup() {
}};
err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_);
if (err != ESP_OK) {
this->smark_failed("esp_lcd_new_panel_dpi failed", err);
this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err);
return;
}
if (this->reset_pin_ != nullptr) {
@@ -86,14 +92,14 @@ void MIPI_DSI::setup() {
auto when = millis() + 120;
err = esp_lcd_panel_init(this->handle_);
if (err != ESP_OK) {
this->smark_failed("esp_lcd_init failed", err);
this->smark_failed(LOG_STR("esp_lcd_init failed"), err);
return;
}
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
this->mark_failed("Malformed init sequence");
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
uint8_t cmd = vec[index++];
@@ -104,7 +110,7 @@ void MIPI_DSI::setup() {
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
this->mark_failed("Malformed init sequence");
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
if (cmd == SLEEP_OUT) {
@@ -119,7 +125,7 @@ void MIPI_DSI::setup() {
format_hex_pretty(ptr, num_args, '.', false).c_str());
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args);
if (err != ESP_OK) {
this->smark_failed("lcd_panel_io_tx_param failed", err);
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
return;
}
index += num_args;
@@ -134,7 +140,7 @@ void MIPI_DSI::setup() {
err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_));
if (err != ESP_OK) {
this->smark_failed("Failed to register callbacks", err);
this->smark_failed(LOG_STR("Failed to register callbacks"), err);
return;
}
@@ -216,7 +222,7 @@ bool MIPI_DSI::check_buffer_() {
RAMAllocator<uint8_t> allocator;
this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel);
if (this->buffer_ == nullptr) {
this->mark_failed("Could not allocate buffer for display!");
this->mark_failed(LOG_STR("Could not allocate buffer for display!"));
return false;
}
return true;
+1 -4
View File
@@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display {
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
void smark_failed(const char *message, esp_err_t err) {
auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err));
this->mark_failed(str.c_str());
}
void smark_failed(const LogString *message, esp_err_t err);
void update() override;
@@ -35,3 +35,70 @@ DriverChip(
(0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00),
],
)
# JC4880P443 Driver Configuration (ST7701)
# Using parameters from esp_lcd_st7701.h and the working full init sequence
# ----------------------------------------------------------------------------------------------------------------------
# * Resolution: 480x800
# * PCLK Frequency: 34 MHz
# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration)
# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42)
# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166)
# ----------------------------------------------------------------------------------------------------------------------
DriverChip(
"JC4880P443",
width=480,
height=800,
hsync_back_porch=42,
hsync_pulse_width=12,
hsync_front_porch=42,
vsync_back_porch=8,
vsync_pulse_width=2,
vsync_front_porch=166,
pclk_frequency="34MHz",
lane_bit_rate="500Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=5,
initsequence=[
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13),
(0xEF, 0x08),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10),
(0xC0, 0x63, 0x00),
(0xC1, 0x0D, 0x02),
(0xC2, 0x10, 0x08),
(0xCC, 0x10),
(0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71),
(0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x11),
(0xB0, 0x5D),
(0xB1, 0x58),
(0xB2, 0x87),
(0xB3, 0x80),
(0xB5, 0x4E),
(0xB7, 0x85),
(0xB8, 0x21),
(0xB9, 0x10, 0x1F),
(0xBB, 0x03),
(0xBC, 0x00),
(0xC1, 0x78),
(0xC2, 0x78),
(0xD0, 0x88),
(0xE0, 0x00, 0x3A, 0x02),
(0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40),
(0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00),
(0xE3, 0x00, 0x00, 0x33, 0x33),
(0xE4, 0x44, 0x44),
(0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0),
(0xE6, 0x00, 0x00, 0x33, 0x33),
(0xE7, 0x44, 0x44),
(0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0),
(0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00),
(0xEC, 0x08, 0x01),
(0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B),
(0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x00),
]
)
# fmt: on
+5 -5
View File
@@ -73,7 +73,7 @@ void MipiRgbSpi::write_init_sequence_() {
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
this->mark_failed("Malformed init sequence");
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
uint8_t cmd = vec[index++];
@@ -84,7 +84,7 @@ void MipiRgbSpi::write_init_sequence_() {
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
this->mark_failed("Malformed init sequence");
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
if (cmd == SLEEP_OUT) {
@@ -164,8 +164,8 @@ void MipiRgb::common_setup_() {
if (err == ESP_OK)
err = esp_lcd_panel_init(this->handle_);
if (err != ESP_OK) {
auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err));
this->mark_failed(msg.c_str());
ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err));
this->mark_failed(LOG_STR("lcd setup failed"));
}
ESP_LOGCONFIG(TAG, "MipiRgb setup complete");
}
@@ -249,7 +249,7 @@ bool MipiRgb::check_buffer_() {
RAMAllocator<uint16_t> allocator;
this->buffer_ = allocator.allocate(this->height_ * this->width_);
if (this->buffer_ == nullptr) {
this->mark_failed("Could not allocate buffer for display!");
this->mark_failed(LOG_STR("Could not allocate buffer for display!"));
return false;
}
return true;
+1 -1
View File
@@ -478,7 +478,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed("Buffer allocation failed");
this->mark_failed(LOG_STR("Buffer allocation failed"));
}
}
@@ -78,19 +78,20 @@ void SourceSpeaker::loop() {
} else {
switch (err) {
case ESP_ERR_NO_MEM:
this->status_set_error("Failed to start mixer: not enough memory");
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
break;
case ESP_ERR_NOT_SUPPORTED:
this->status_set_error("Failed to start mixer: unsupported bits per sample");
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
break;
case ESP_ERR_INVALID_ARG:
this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream.");
this->status_set_error(
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
break;
case ESP_ERR_INVALID_STATE:
this->status_set_error("Failed to start mixer: mixer task failed to start");
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
break;
default:
this->status_set_error("Failed to start mixer");
this->status_set_error(LOG_STR("Failed to start mixer"));
break;
}
@@ -317,7 +318,7 @@ void MixerSpeaker::loop() {
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
}
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error("Failed to allocate the mixer's internal buffer");
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
}
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
+1 -1
View File
@@ -140,7 +140,7 @@ void MQTTClientComponent::send_device_info_() {
#endif
#ifdef USE_API_NOISE
root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] =
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
#endif
},
+1 -1
View File
@@ -278,7 +278,7 @@ void NAU7802Sensor::loop() {
this->set_calibration_failure_(true);
this->state_ = CalibrationState::INACTIVE;
ESP_LOGE(TAG, "Failed to calibrate sensor");
this->status_set_error("Calibration Failed");
this->status_set_error(LOG_STR("Calibration Failed"));
return;
}
@@ -2,6 +2,7 @@
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color);
// Feed watchdog periodically to avoid triggering during long decode operations.
// Feed every 1024 pixels to balance efficiency and responsiveness.
uint32_t pixels = w * h;
decoder->increment_pixels_decoded(pixels);
if ((decoder->get_pixels_decoded() % 1024) < pixels) {
App.feed_wdt();
}
}
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
@@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder {
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
uint32_t get_pixels_decoded() const { return this->pixels_decoded_; }
protected:
RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_;
uint32_t pixels_decoded_{0};
};
} // namespace online_image
@@ -195,7 +195,7 @@ static void add(std::vector<uint8_t> &vec, const char *str) {
void PacketTransport::setup() {
this->name_ = App.get_name().c_str();
if (strlen(this->name_) > 255) {
this->status_set_error("Device name exceeds 255 chars");
this->status_set_error(LOG_STR("Device name exceeds 255 chars"));
this->mark_failed();
return;
}
@@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
this->lock_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_EVENT
this->event_type_(stream);
for (auto *obj : App.get_events())
this->event_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_TEXT
this->text_type_(stream);
for (auto *obj : App.get_texts())
this->text_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_TEXT_SENSOR
this->text_sensor_type_(stream);
for (auto *obj : App.get_text_sensors())
@@ -547,6 +559,100 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
}
#endif
// Type-specific implementation
#ifdef USE_TEXT
void PrometheusHandler::text_type_(AsyncResponseStream *stream) {
stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n"));
stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n"));
}
void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node,
std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->has_state()) {
// We have a valid value, output this value
stream->print(ESPHOME_F("esphome_text_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 0\n"));
// Data itself
stream->print(ESPHOME_F("esphome_text_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",value=\""));
stream->print(obj->state.c_str());
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
} else {
// Invalid state
stream->print(ESPHOME_F("esphome_text_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 1\n"));
}
}
#endif
// Type-specific implementation
#ifdef USE_EVENT
void PrometheusHandler::event_type_(AsyncResponseStream *stream) {
stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n"));
stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n"));
}
void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node,
std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->get_last_event_type() != nullptr) {
// We have a valid event type, output this value
stream->print(ESPHOME_F("esphome_event_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 0\n"));
// Data itself
stream->print(ESPHOME_F("esphome_event_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",last_event_type=\""));
stream->print(obj->get_last_event_type());
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
} else {
// No event triggered yet
stream->print(ESPHOME_F("esphome_event_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 1\n"));
}
}
#endif
// Type-specific implementation
#ifdef USE_NUMBER
void PrometheusHandler::number_type_(AsyncResponseStream *stream) {
@@ -620,7 +726,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",value=\""));
stream->print(obj->state.c_str());
stream->print(obj->current_option());
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
@@ -123,6 +123,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
std::string &friendly_name);
#endif
#ifdef USE_EVENT
/// Return the type for prometheus
void event_type_(AsyncResponseStream *stream);
/// Return the event values state as prometheus data point
void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
#ifdef USE_TEXT
/// Return the type for prometheus
void text_type_(AsyncResponseStream *stream);
/// Return the text values state as prometheus data point
void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
#ifdef USE_TEXT_SENSOR
/// Return the type for prometheus
void text_sensor_type_(AsyncResponseStream *stream);
+1 -1
View File
@@ -310,7 +310,7 @@ void QMP6988Component::calculate_pressure_() {
void QMP6988Component::setup() {
if (!this->device_check_()) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
@@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() {
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error("Resampler task failed to allocate the internal buffers");
this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
this->state_ = speaker::STATE_STOPPING;
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
this->status_set_error("Cannot resample due to an unsupported audio stream");
this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
this->state_ = speaker::STATE_STOPPING;
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
this->status_set_error("Resampler task failed");
this->status_set_error(LOG_STR("Resampler task failed"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
this->state_ = speaker::STATE_STOPPING;
}
@@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() {
} else {
switch (err) {
case ESP_ERR_INVALID_STATE:
this->status_set_error("Failed to start resampler: resampler task failed to start");
this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start"));
break;
case ESP_ERR_NO_MEM:
this->status_set_error("Failed to start resampler: not enough memory for task stack");
this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack"));
default:
this->status_set_error("Failed to start resampler");
this->status_set_error(LOG_STR("Failed to start resampler"));
break;
}
+12 -8
View File
@@ -1,8 +1,8 @@
#pragma once
#include <list>
#include <memory>
#include <tuple>
#include <forward_list>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
@@ -290,10 +290,10 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
}
// Store parameters for later execution
this->param_queue_.emplace_front(x...);
// Enable loop now that we have work to do
this->param_queue_.emplace_back(x...);
// Enable loop now that we have work to do - don't call loop() synchronously!
// Let the event loop call it to avoid reentrancy issues
this->enable_loop();
this->loop();
}
void loop() override {
@@ -303,13 +303,17 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
if (this->script_->is_running())
return;
while (!this->param_queue_.empty()) {
// Only process ONE queued item per loop iteration
// Processing all items in a while loop causes infinite loops because
// play_next_() can trigger more items to be queued
if (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
this->param_queue_.pop_front();
} else {
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
}
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
}
void play(const Ts &...x) override { /* ignore - see play_complex */
@@ -326,7 +330,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
}
C *script_;
std::forward_list<std::tuple<Ts...>> param_queue_;
std::list<std::tuple<Ts...>> param_queue_;
};
} // namespace script
+1 -1
View File
@@ -13,7 +13,7 @@ void SHT4XComponent::start_heater_() {
ESP_LOGD(TAG, "Heater turning on");
if (this->write(cmd, 1) != i2c::ERROR_OK) {
this->status_set_error("Failed to turn on heater");
this->status_set_error(LOG_STR("Failed to turn on heater"));
}
}
+6 -6
View File
@@ -21,7 +21,7 @@ static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsiu
void STTS22HComponent::setup() {
// Check if device is a STTS22H
if (!this->is_stts22h_sensor_()) {
this->mark_failed("Device is not a STTS22H sensor");
this->mark_failed(LOG_STR("Device is not a STTS22H sensor"));
return;
}
@@ -61,12 +61,12 @@ float STTS22HComponent::read_temperature_() {
bool STTS22HComponent::is_stts22h_sensor_() {
uint8_t whoami_value;
if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) {
this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H");
this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H"));
return false;
}
@@ -77,7 +77,7 @@ void STTS22HComponent::initialize_sensor_() {
// Read current CTRL_REG configuration
uint8_t ctrl_value;
if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
@@ -86,14 +86,14 @@ void STTS22HComponent::initialize_sensor_() {
// FREERUN bit must be cleared (see sensor documentation)
ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit
if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
// Enable LOW ODR mode and ADD_INC
ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit
if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
}
@@ -0,0 +1,97 @@
import esphome.codegen as cg
from esphome.components import esp32_ble_tracker, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_EXTERNAL_TEMPERATURE,
CONF_HUMIDITY,
CONF_ID,
CONF_MAC_ADDRESS,
CONF_SIGNAL_STRENGTH,
CONF_TEMPERATURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_DECIBEL_MILLIWATT,
UNIT_PERCENT,
)
CODEOWNERS = ["@sittner"]
DEPENDENCIES = ["esp32_ble_tracker"]
thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble")
ThermoProBLE = thermopro_ble_ns.class_(
"ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ThermoProBLE),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_BATTERY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
sens = await sensor.new_sensor(external_temperature_config)
cg.add(var.set_external_temperature(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery_level(sens))
if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
sens = await sensor.new_sensor(signal_strength_config)
cg.add(var.set_signal_strength(sens))
@@ -0,0 +1,204 @@
#include "thermopro_ble.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
namespace esphome::thermopro_ble {
// this size must be large enough to hold the largest data frame
// of all supported devices
static constexpr std::size_t MAX_DATA_SIZE = 24;
struct DeviceParserMapping {
const char *prefix;
DeviceParser parser;
};
static float tp96_battery(uint16_t voltage);
static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size);
static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size);
static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size);
static const char *const TAG = "thermopro_ble";
static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = {
{"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}};
void ThermoProBLE::dump_config() {
ESP_LOGCONFIG(TAG, "ThermoPro BLE");
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "External temperature", this->external_temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
}
bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
// check for matching mac address
if (device.address_uint64() != this->address_) {
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
return false;
}
// check for valid device type
update_device_type_(device.get_name());
if (this->device_parser_ == nullptr) {
ESP_LOGVV(TAG, "parse_device(): invalid device type.");
return false;
}
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
// publish signal strength
float signal_strength = float(device.get_rssi());
if (this->signal_strength_ != nullptr)
this->signal_strength_->publish_state(signal_strength);
bool success = false;
for (auto &service_data : device.get_manufacturer_datas()) {
// check maximum data size
std::size_t data_size = service_data.data.size() + 2;
if (data_size > MAX_DATA_SIZE) {
ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!");
continue;
}
// reconstruct whole record from 2 byte uuid and data
esp_bt_uuid_t uuid = service_data.uuid.get_uuid();
uint8_t data[MAX_DATA_SIZE] = {static_cast<uint8_t>(uuid.uuid.uuid16), static_cast<uint8_t>(uuid.uuid.uuid16 >> 8)};
std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2);
// dispatch data to parser
optional<ParseResult> result = this->device_parser_(data, data_size);
if (!result.has_value()) {
continue;
}
// publish sensor values
if (result->temperature.has_value() && this->temperature_ != nullptr)
this->temperature_->publish_state(*result->temperature);
if (result->external_temperature.has_value() && this->external_temperature_ != nullptr)
this->external_temperature_->publish_state(*result->external_temperature);
if (result->humidity.has_value() && this->humidity_ != nullptr)
this->humidity_->publish_state(*result->humidity);
if (result->battery_level.has_value() && this->battery_level_ != nullptr)
this->battery_level_->publish_state(*result->battery_level);
success = true;
}
return success;
}
void ThermoProBLE::update_device_type_(const std::string &device_name) {
// check for changed device name (should only happen on initial call)
if (this->device_name_ == device_name) {
return;
}
// remember device name
this->device_name_ = device_name;
// try to find device parser
for (const auto &mapping : DEVICE_PARSER_MAP) {
if (device_name.starts_with(mapping.prefix)) {
this->device_parser_ = mapping.parser;
return;
}
}
// device type unknown
this->device_parser_ = nullptr;
ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str());
}
static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) {
return static_cast<uint16_t>(data[offset + 0]) | (static_cast<uint16_t>(data[offset + 1]) << 8);
}
static inline int16_t read_int16(const uint8_t *data, std::size_t offset) {
return static_cast<int16_t>(read_uint16(data, offset));
}
static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) {
return static_cast<uint32_t>(data[offset + 0]) | (static_cast<uint32_t>(data[offset + 1]) << 8) |
(static_cast<uint32_t>(data[offset + 2]) << 16) | (static_cast<uint32_t>(data[offset + 3]) << 24);
}
// Battery calculation used with permission from:
// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py
//
// TP96x battery values appear to be a voltage reading, probably in millivolts.
// This means that calculating battery life from it is a non-linear function.
// Examining the curve, it looked fairly close to a curve from the tanh function.
// So, I created a script to use Tensorflow to optimize an equation in the format
// A*tanh(B*x+C)+D
// Where A,B,C,D are the variables to optimize for. This yielded the below function
static float tp96_battery(uint16_t voltage) {
float level = 52.317286f * tanh(static_cast<float>(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f;
return std::max(0.0f, std::min(level, 100.0f));
}
static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size) {
if (data_size != 23) {
ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size);
return {};
}
ParseResult result;
// ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset
result.external_temperature = static_cast<float>(read_uint16(data, 1)) - 54.0f;
// battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
result.battery_level = tp96_battery(read_uint16(data, 3));
// internal temperature, 4 bytes, float, -54 °C offset
result.temperature = static_cast<float>(read_uint32(data, 9)) - 54.0f;
return result;
}
static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size) {
if (data_size != 7) {
ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size);
return {};
}
ParseResult result;
// internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
result.temperature = static_cast<float>(read_uint16(data, 1)) - 30.0f;
// battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
result.battery_level = tp96_battery(read_uint16(data, 3));
// ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
result.external_temperature = static_cast<float>(read_uint16(data, 5)) - 30.0f;
return result;
}
static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size) {
if (data_size < 6) {
ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size);
return {};
}
ParseResult result;
// temperature, 2 bytes, 16-bit signed integer, 0.1 °C
result.temperature = static_cast<float>(read_int16(data, 1)) * 0.1f;
// humidity, 1 byte, 8-bit unsigned integer, 1.0 %
result.humidity = static_cast<float>(data[3]);
// battery level, 2 bits (0-2)
result.battery_level = static_cast<float>(data[4] & 0x3) * 50.0;
return result;
}
} // namespace esphome::thermopro_ble
#endif
@@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#ifdef USE_ESP32
namespace esphome::thermopro_ble {
struct ParseResult {
optional<float> temperature;
optional<float> external_temperature;
optional<float> humidity;
optional<float> battery_level;
};
using DeviceParser = optional<ParseResult> (*)(const uint8_t *data, std::size_t data_size);
class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; };
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
void dump_config() override;
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_external_temperature(sensor::Sensor *external_temperature) {
this->external_temperature_ = external_temperature;
}
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
protected:
uint64_t address_;
std::string device_name_;
DeviceParser device_parser_{nullptr};
sensor::Sensor *signal_strength_{nullptr};
sensor::Sensor *temperature_{nullptr};
sensor::Sensor *external_temperature_{nullptr};
sensor::Sensor *humidity_{nullptr};
sensor::Sensor *battery_level_{nullptr};
void update_device_type_(const std::string &device_name);
};
} // namespace esphome::thermopro_ble
#endif
+5 -5
View File
@@ -21,7 +21,7 @@ void UDPComponent::setup() {
if (this->should_broadcast_) {
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (this->broadcast_socket_ == nullptr) {
this->status_set_error("Could not create socket");
this->status_set_error(LOG_STR("Could not create socket"));
this->mark_failed();
return;
}
@@ -41,14 +41,14 @@ void UDPComponent::setup() {
if (this->should_listen_) {
this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (this->listen_socket_ == nullptr) {
this->status_set_error("Could not create socket");
this->status_set_error(LOG_STR("Could not create socket"));
this->mark_failed();
return;
}
auto err = this->listen_socket_->setblocking(false);
if (err < 0) {
ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno);
this->status_set_error("Unable to set nonblocking");
this->status_set_error(LOG_STR("Unable to set nonblocking"));
this->mark_failed();
return;
}
@@ -73,7 +73,7 @@ void UDPComponent::setup() {
err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq));
if (err < 0) {
ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno);
this->status_set_error("Failed to set IP_ADD_MEMBERSHIP");
this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP"));
this->mark_failed();
return;
}
@@ -82,7 +82,7 @@ void UDPComponent::setup() {
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
this->status_set_error("Unable to bind socket");
this->status_set_error(LOG_STR("Unable to bind socket"));
this->mark_failed();
return;
}
@@ -188,7 +188,7 @@ void USBClient::setup() {
auto err = usb_host_client_register(&config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err));
this->status_set_error("Client register failed");
this->status_set_error(LOG_STR("Client register failed"));
this->mark_failed();
return;
}
@@ -11,7 +11,7 @@ void USBHost::setup() {
usb_host_config_t config{};
if (usb_host_install(&config) != ESP_OK) {
this->status_set_error("usb_host_install failed");
this->status_set_error(LOG_STR("usb_host_install failed"));
this->mark_failed();
return;
}
+2 -2
View File
@@ -320,7 +320,7 @@ static void fix_mps(const usb_ep_desc_t *ep) {
void USBUartTypeCdcAcm::on_connected() {
auto cdc_devs = this->parse_descriptors(this->device_handle_);
if (cdc_devs.empty()) {
this->status_set_error("No CDC-ACM device found");
this->status_set_error(LOG_STR("No CDC-ACM device found"));
this->disconnect();
return;
}
@@ -341,7 +341,7 @@ void USBUartTypeCdcAcm::on_connected() {
if (err != ESP_OK) {
ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
channel->cdc_dev_.bulk_interface_number);
this->status_set_error("usb_host_interface_claim failed");
this->status_set_error(LOG_STR("usb_host_interface_claim failed"));
this->disconnect();
return;
}
@@ -206,7 +206,7 @@ void VoiceAssistant::loop() {
case State::START_MICROPHONE: {
ESP_LOGD(TAG, "Starting Microphone");
if (!this->allocate_buffers_()) {
this->status_set_error("Failed to allocate buffers");
this->status_set_error(LOG_STR("Failed to allocate buffers"));
return;
}
if (this->status_has_error()) {
@@ -67,7 +67,7 @@ void WakeOnLanButton::setup() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (this->broadcast_socket_ == nullptr) {
this->status_set_error("Could not create socket");
this->status_set_error(LOG_STR("Could not create socket"));
this->mark_failed();
return;
}
@@ -111,7 +111,7 @@ class WebServerBase : public Component {
this->initialized_++;
return;
}
this->server_ = std::make_shared<AsyncWebServer>(this->port_);
this->server_ = std::make_unique<AsyncWebServer>(this->port_);
// All content is controlled and created by user - so allowing all origins is fine here.
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
this->server_->begin();
@@ -127,7 +127,7 @@ class WebServerBase : public Component {
this->server_ = nullptr;
}
}
std::shared_ptr<AsyncWebServer> get_server() const { return server_; }
AsyncWebServer *get_server() const { return this->server_.get(); }
float get_setup_priority() const override;
#ifdef USE_WEBSERVER_AUTH
@@ -143,7 +143,7 @@ class WebServerBase : public Component {
protected:
int initialized_{0};
uint16_t port_{80};
std::shared_ptr<AsyncWebServer> server_{nullptr};
std::unique_ptr<AsyncWebServer> server_{nullptr};
std::vector<AsyncWebHandler *> handlers_;
#ifdef USE_WEBSERVER_AUTH
internal::Credentials credentials_;
+23 -1
View File
@@ -97,6 +97,7 @@ WIFI_MIN_AUTH_MODES = {
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
WiFiConfigureAction = wifi_ns.class_(
@@ -590,6 +591,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({}))
async def wifi_ap_active_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({}))
async def wifi_enable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg)
@@ -601,6 +607,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
def request_wifi_scan_results():
@@ -613,13 +620,28 @@ def request_wifi_scan_results():
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
def enable_runtime_power_save_control():
"""Enable runtime WiFi power save control.
Components that need to dynamically switch WiFi power saving on/off for latency
performance (e.g., audio streaming, large data transfers) should call this
function during their code generation. This enables the request_high_performance()
and release_high_performance() APIs.
Only supported on ESP32.
"""
CORE.data[RUNTIME_POWER_SAVE_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure scan result retention."""
"""Final code generation step to configure optional WiFi features."""
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
cg.add(
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
)
if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False):
cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE")
@automation.register_action(
+5
View File
@@ -16,6 +16,11 @@ template<typename... Ts> class WiFiEnabledCondition : public Condition<Ts...> {
bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); }
};
template<typename... Ts> class WiFiAPActiveCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); }
};
template<typename... Ts> class WiFiEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->enable(); }
+90 -1
View File
@@ -329,6 +329,19 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() {
this->wifi_pre_setup_();
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Create semaphore for high-performance mode requests
// Start at 0, increment on request, decrement on release
this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
if (this->high_performance_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Failed semaphore");
}
// Store the configured power save mode as baseline
this->configured_power_save_ = this->power_save_;
#endif
if (this->enable_on_boot_) {
this->start();
} else {
@@ -370,6 +383,19 @@ void WiFiComponent::start() {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Synchronize power_save_ with semaphore state before applying
if (this->high_performance_semaphore_ != nullptr) {
UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
if (semaphore_count > 0) {
this->power_save_ = WIFI_POWER_SAVE_NONE;
this->is_high_performance_mode_ = true;
} else {
this->power_save_ = this->configured_power_save_;
this->is_high_performance_mode_ = false;
}
}
#endif
if (!this->wifi_apply_power_save_()) {
ESP_LOGV(TAG, "Setting Power Save Option failed");
}
@@ -524,11 +550,37 @@ void WiFiComponent::loop() {
}
}
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Check if power save mode needs to be updated based on high-performance requests
if (this->high_performance_semaphore_ != nullptr) {
// Semaphore count directly represents active requests (starts at 0, increments on request)
UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
if (semaphore_count > 0 && !this->is_high_performance_mode_) {
// Transition to high-performance mode (no power save)
ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
semaphore_count == 1 ? "request" : "requests");
this->power_save_ = WIFI_POWER_SAVE_NONE;
if (this->wifi_apply_power_save_()) {
this->is_high_performance_mode_ = true;
}
} else if (semaphore_count == 0 && this->is_high_performance_mode_) {
// Restore to configured power save mode
ESP_LOGV(TAG, "Restoring power save mode to configured setting");
this->power_save_ = this->configured_power_save_;
if (this->wifi_apply_power_save_()) {
this->is_high_performance_mode_ = false;
}
}
}
#endif
}
WiFiComponent::WiFiComponent() { global_wifi_component = this; }
bool WiFiComponent::has_ap() const { return this->has_ap_; }
bool WiFiComponent::is_ap_active() const { return this->state_ == WIFI_COMPONENT_STATE_AP; }
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
#ifdef USE_WIFI_11KV_SUPPORT
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
@@ -1565,7 +1617,12 @@ bool WiFiComponent::is_connected() {
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
}
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; }
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
this->power_save_ = power_save;
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
this->configured_power_save_ = power_save;
#endif
}
void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
@@ -1584,6 +1641,38 @@ bool WiFiComponent::is_esp32_improv_active_() {
#endif
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
bool WiFiComponent::request_high_performance() {
// Already configured for high performance - request satisfied
if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) {
return true;
}
// Semaphore initialization failed
if (this->high_performance_semaphore_ == nullptr) {
return false;
}
// Give the semaphore (non-blocking). This increments the count.
return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
}
bool WiFiComponent::release_high_performance() {
// Already configured for high performance - nothing to release
if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) {
return true;
}
// Semaphore initialization failed
if (this->high_performance_semaphore_ == nullptr) {
return false;
}
// Take the semaphore (non-blocking). This decrements the count.
return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
}
#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
#ifdef USE_WIFI_FAST_CONNECT
bool WiFiComponent::load_fast_connect_settings_(WiFiAP &params) {
SavedWifiFastConnectSettings fast_connect_save{};
+43
View File
@@ -49,6 +49,11 @@ extern "C" {
#include <WiFi.h>
#endif
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#endif
namespace esphome::wifi {
/// Sentinel value for RSSI when WiFi is not connected
@@ -307,6 +312,7 @@ class WiFiComponent : public Component {
bool has_sta() const;
bool has_ap() const;
bool is_ap_active() const;
#ifdef USE_WIFI_11KV_SUPPORT
void set_btm(bool btm);
@@ -382,6 +388,37 @@ class WiFiComponent : public Component {
this->wifi_connect_state_callback_.add(std::move(callback));
}
#ifdef USE_WIFI_RUNTIME_POWER_SAVE
/** Request high-performance mode (no power saving) for improved WiFi latency.
*
* Components that need maximum WiFi performance (e.g., audio streaming, large data transfers)
* can call this method to temporarily disable WiFi power saving. Multiple components can
* request high performance simultaneously using a counting semaphore.
*
* Power saving will be restored to the YAML-configured mode when all components have
* called release_high_performance().
*
* Note: Only supported on ESP32.
*
* @return true if request was satisfied (high-performance mode active or already configured),
* false if operation failed (semaphore error)
*/
bool request_high_performance();
/** Release a high-performance mode request.
*
* Should be called when a component no longer needs maximum WiFi latency.
* When all requests are released (semaphore count reaches zero), WiFi power saving
* is restored to the YAML-configured mode.
*
* Note: Only supported on ESP32.
*
* @return true if release was successful (or already in high-performance config),
* false if operation failed (semaphore error)
*/
bool release_high_performance();
#endif // USE_WIFI_RUNTIME_POWER_SAVE
protected:
#ifdef USE_WIFI_AP
void setup_ap_config_();
@@ -555,6 +592,12 @@ class WiFiComponent : public Component {
bool keep_scan_results_{false};
bool did_scan_this_cycle_{false};
bool skip_cooldown_next_cycle_{false};
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE};
bool is_high_performance_mode_{false};
SemaphoreHandle_t high_performance_semaphore_{nullptr};
#endif
// Pointers at the end (naturally aligned)
Trigger<> *connect_trigger_{new Trigger<>()};
@@ -602,10 +602,6 @@ const char *get_auth_mode_str(uint8_t mode) {
}
}
std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); }
#if LWIP_IPV6
std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); }
#endif /* LWIP_IPV6 */
const char *get_disconnect_reason_str(uint8_t reason) {
switch (reason) {
case WIFI_REASON_AUTH_EXPIRE:
@@ -762,15 +758,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
#if USE_NETWORK_IPV6
esp_netif_create_ip6_linklocal(s_sta_netif);
#endif /* USE_NETWORK_IPV6 */
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(),
format_ip4_addr(it.ip_info.gw).c_str());
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
this->got_ipv4_address_ = true;
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#if USE_NETWORK_IPV6
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) {
const auto &it = data->data.ip_got_ip6;
ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str());
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip));
this->num_ipv6_addresses_++;
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#endif /* USE_NETWORK_IPV6 */
@@ -836,7 +831,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
const auto &it = data->data.ip_ap_staipassigned;
ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str());
ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip));
}
}
+5 -4
View File
@@ -9,8 +9,8 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include <list>
#include <vector>
#include <forward_list>
namespace esphome {
@@ -445,9 +445,10 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
// Store for later processing
auto now = millis();
auto timeout = this->timeout_value_.optional_value(x...);
this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...));
this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...));
// Do immediate check with fresh timestamp
// Do immediate check with fresh timestamp - don't call loop() synchronously!
// Let the event loop call it to avoid reentrancy issues
if (this->process_queue_(now)) {
// Only enable loop if we still have pending items
this->enable_loop();
@@ -499,7 +500,7 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
}
Condition<Ts...> *condition_;
std::forward_list<std::tuple<uint32_t, optional<uint32_t>, std::tuple<Ts...>>> var_queue_{};
std::list<std::tuple<uint32_t, optional<uint32_t>, std::tuple<Ts...>>> var_queue_{};
};
template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
+41 -14
View File
@@ -36,6 +36,9 @@ namespace {
struct ComponentErrorMessage {
const Component *component;
const char *message;
// Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer
// Remove before 2026.6.0 when deprecated const char* API is removed
bool is_flash_ptr;
};
struct ComponentPriorityOverride {
@@ -49,6 +52,25 @@ std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
// Setup priority overrides - freed after setup completes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
// Helper to store error messages - reduces duplication between deprecated and new API
// Remove before 2026.6.0 when deprecated const char* API is removed
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
if (entry.component == component) {
entry.message = message;
entry.is_flash_ptr = is_flash_ptr;
return;
}
}
// Add new error message
component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr});
}
} // namespace
namespace setup_priority {
@@ -143,16 +165,20 @@ void Component::call_dump_config() {
if (this->is_failed()) {
// Look up error message from global vector
const char *error_msg = nullptr;
bool is_flash_ptr = false;
if (component_error_messages) {
for (const auto &entry : *component_error_messages) {
if (entry.component == this) {
error_msg = entry.message;
is_flash_ptr = entry.is_flash_ptr;
break;
}
}
}
// Log with appropriate format based on pointer type
ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()),
error_msg ? error_msg : LOG_STR_LITERAL("unspecified"));
error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg)
: LOG_STR_LITERAL("unspecified"));
}
}
@@ -307,6 +333,7 @@ void Component::status_set_warning(const LogString *message) {
ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
}
void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); }
void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
@@ -315,19 +342,19 @@ void Component::status_set_error(const char *message) {
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? message : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
if (entry.component == this) {
entry.message = message;
return;
}
}
// Add new error message
component_error_messages->emplace_back(ComponentErrorMessage{this, message});
store_component_error_message(this, message, false);
}
}
void Component::status_set_error(const LogString *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
// Store the LogString pointer directly (safe because LogString is always in flash/static memory)
store_component_error_message(this, LOG_STR_ARG(message), true);
}
}
void Component::status_clear_warning() {
+20 -1
View File
@@ -5,6 +5,7 @@
#include <functional>
#include <string>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/optional.h"
@@ -157,7 +158,19 @@ class Component {
*/
virtual void mark_failed();
// Remove before 2026.6.0
ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary "
"strings. Will stop working in 2026.6.0",
"2025.12.0")
void mark_failed(const char *message) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->status_set_error(message);
#pragma GCC diagnostic pop
this->mark_failed();
}
void mark_failed(const LogString *message) {
this->status_set_error(message);
this->mark_failed();
}
@@ -216,7 +229,13 @@ class Component {
void status_set_warning(const char *message = nullptr);
void status_set_warning(const LogString *message);
void status_set_error(const char *message = nullptr);
void status_set_error(); // Set error flag without message
// Remove before 2026.6.0
ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary "
"strings. Will stop working in 2026.6.0",
"2025.12.0")
void status_set_error(const char *message);
void status_set_error(const LogString *message);
void status_clear_warning();
+2
View File
@@ -210,12 +210,14 @@
#define USE_WEBSERVER_SORTING
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_RUNTIME_POWER_SAVE
#define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
#define USE_ETHERNET
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_MANUAL_IP
#endif
#ifdef USE_ESP_IDF
+7 -1
View File
@@ -343,7 +343,13 @@ def clean_build(clear_pio_cache: bool = True):
def clean_all(configuration: list[str]):
import shutil
data_dirs = [Path(dir) / ".esphome" for dir in configuration]
data_dirs = []
for config in configuration:
item = Path(config)
if item.is_file() and item.suffix in (".yaml", ".yml"):
data_dirs.append(item.parent / ".esphome")
else:
data_dirs.append(item / ".esphome")
if is_ha_addon():
data_dirs.append(Path("/data"))
if "ESPHOME_DATA_DIR" in os.environ:
+1 -1
View File
@@ -22,7 +22,7 @@ pillow==11.3.0
cairosvg==2.8.2
freetype-py==2.5.1
jinja2==3.1.6
bleak==1.1.1
bleak==2.0.0
# esp-idf >= 5.0 requires this
pyparsing >= 3.0
+4
View File
@@ -1,6 +1,10 @@
esphome:
on_boot:
then:
- wait_until:
condition:
api.connected:
state_subscription_only: true
- homeassistant.event:
event: esphome.button_pressed
data:
+28
View File
@@ -39,6 +39,15 @@ sensor:
return 0.0;
update_interval: 60s
text:
- platform: template
name: "Template text"
optimistic: true
min_length: 0
max_length: 100
mode: text
initial_value: "Hello World"
text_sensor:
- platform: version
name: "ESPHome Version"
@@ -52,6 +61,25 @@ text_sensor:
return {"Goodbye (cruel) World"};
update_interval: 60s
event:
- platform: template
name: "Template Event"
id: template_event1
event_types:
- "custom_event_1"
- "custom_event_2"
button:
- platform: template
name: "Template Event Button"
on_press:
- logger.log: "Template Event Button pressed"
- lambda: |-
ESP_LOGD("template_event_button", "Template Event Button pressed");
- event.trigger:
id: template_event1
event_type: custom_event_1
binary_sensor:
- platform: template
id: template_binary_sensor1
@@ -0,0 +1,13 @@
esp32_ble_tracker:
sensor:
- platform: thermopro_ble
mac_address: FE:74:B8:6A:97:B7
temperature:
name: "ThermoPro Temperature"
humidity:
name: "ThermoPro Humidity"
battery_level:
name: "ThermoPro Battery Level"
signal_strength:
name: "ThermoPro Signal Strength"
@@ -0,0 +1,4 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
<<: !include common.yaml
+4
View File
@@ -10,6 +10,10 @@ esphome:
- logger.log: "Connected to WiFi!"
on_error:
- logger.log: "Failed to connect to WiFi!"
- if:
condition: wifi.ap_active
then:
- logger.log: "WiFi AP is active!"
wifi:
networks:
+11
View File
@@ -1,5 +1,16 @@
psram:
# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define
esphome:
platformio_options:
build_flags:
- "-DUSE_WIFI_RUNTIME_POWER_SAVE"
on_boot:
- then:
- lambda: |-
esphome::wifi::global_wifi_component->request_high_performance();
esphome::wifi::global_wifi_component->release_high_performance();
wifi:
use_psram: true
min_auth_mode: WPA
@@ -0,0 +1,131 @@
esphome:
name: test-script-delay-params
host:
api:
actions:
# Test case from issue #12044: parent script with repeat calling child with delay
- action: test_repeat_with_delay
then:
- logger.log: "=== TEST: Repeat loop calling script with delay and parameters ==="
- script.execute: father_script
# Test case from issue #12043: script.wait with delayed child script
- action: test_script_wait
then:
- logger.log: "=== TEST: script.wait with delayed child script ==="
- script.execute: show_start_page
- script.wait: show_start_page
- logger.log: "After wait: script completed successfully"
# Test: Delay with different parameter types
- action: test_delay_param_types
then:
- logger.log: "=== TEST: Delay with various parameter types ==="
- script.execute:
id: delay_with_int
val: 42
- delay: 50ms
- script.execute:
id: delay_with_string
msg: "test message"
- delay: 50ms
- script.execute:
id: delay_with_float
num: 3.14
logger:
level: DEBUG
script:
# Reproduces issue #12044: child script with conditional delay
- id: son_script
mode: single
parameters:
iteration: int
then:
- logger.log:
format: "Son script started with iteration %d"
args: ['iteration']
- if:
condition:
lambda: 'return iteration >= 5;'
then:
- logger.log:
format: "Son script delaying for iteration %d"
args: ['iteration']
- delay: 100ms
- logger.log:
format: "Son script finished with iteration %d"
args: ['iteration']
# Reproduces issue #12044: parent script with repeat loop
- id: father_script
mode: single
then:
- repeat:
count: 10
then:
- logger.log:
format: "Father iteration %d: calling son"
args: ['iteration']
- script.execute:
id: son_script
iteration: !lambda 'return iteration;'
- script.wait: son_script
- logger.log:
format: "Father iteration %d: son finished, wait returned"
args: ['iteration']
# Reproduces issue #12043: script.wait hangs
- id: show_start_page
mode: single
then:
- logger.log: "Start page: beginning"
- delay: 100ms
- logger.log: "Start page: after delay"
- delay: 100ms
- logger.log: "Start page: completed"
# Test delay with int parameter
- id: delay_with_int
mode: single
parameters:
val: int
then:
- logger.log:
format: "Int test: before delay, val=%d"
args: ['val']
- delay: 50ms
- logger.log:
format: "Int test: after delay, val=%d"
args: ['val']
# Test delay with string parameter
- id: delay_with_string
mode: single
parameters:
msg: string
then:
- logger.log:
format: "String test: before delay, msg=%s"
args: ['msg.c_str()']
- delay: 50ms
- logger.log:
format: "String test: after delay, msg=%s"
args: ['msg.c_str()']
# Test delay with float parameter
- id: delay_with_float
mode: single
parameters:
num: float
then:
- logger.log:
format: "Float test: before delay, num=%.2f"
args: ['num']
- delay: 50ms
- logger.log:
format: "Float test: after delay, num=%.2f"
args: ['num']
@@ -0,0 +1,82 @@
esphome:
name: test-wait-until-ordering
host:
api:
actions:
- action: test_wait_until_fifo
then:
- logger.log: "=== TEST: wait_until should execute in FIFO order ==="
- globals.set:
id: gate_open
value: 'false'
- delay: 100ms
# Start multiple parallel executions of coordinator script
# Each will call the shared waiter script, queueing in same wait_until
- script.execute: coordinator_0
- script.execute: coordinator_1
- script.execute: coordinator_2
- script.execute: coordinator_3
- script.execute: coordinator_4
# Give scripts time to reach wait_until and queue
- delay: 200ms
- logger.log: "Opening gate - all wait_until should complete now"
- globals.set:
id: gate_open
value: 'true'
- delay: 500ms
- logger.log: "Test complete"
globals:
- id: gate_open
type: bool
initial_value: 'false'
script:
# Shared waiter with single wait_until action (all coordinators call this)
- id: waiter
mode: parallel
parameters:
iter: int
then:
- lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);'
- wait_until:
condition:
lambda: 'return id(gate_open);'
timeout: 5s
- lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);'
# Coordinator scripts - each calls shared waiter with different iteration number
- id: coordinator_0
then:
- script.execute:
id: waiter
iter: 0
- id: coordinator_1
then:
- script.execute:
id: waiter
iter: 1
- id: coordinator_2
then:
- script.execute:
id: waiter
iter: 2
- id: coordinator_3
then:
- script.execute:
id: waiter
iter: 3
- id: coordinator_4
then:
- script.execute:
id: waiter
iter: 4
logger:
level: DEBUG
@@ -0,0 +1,121 @@
"""Integration test for script.wait FIFO ordering (issues #12043, #12044).
This test verifies that ScriptWaitAction processes queued items in FIFO order.
PR #7972 introduced bugs in ScriptWaitAction:
- Used emplace_front() causing LIFO ordering instead of FIFO
- Called loop() synchronously causing reentrancy issues
- Used while loop processing entire queue causing infinite loops
These bugs manifested as:
- Scripts becoming "zombies" (stuck in running state)
- script.wait hanging forever
- Incorrect execution order
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_script_delay_with_params(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that script.wait processes queued items in FIFO order.
This reproduces issues #12043 and #12044 where scripts would hang or become
zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972.
"""
test_complete = asyncio.Event()
# Patterns to match in logs
father_calling_pattern = re.compile(r"Father iteration (\d+): calling son")
son_started_pattern = re.compile(r"Son script started with iteration (\d+)")
son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)")
son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)")
father_wait_returned_pattern = re.compile(
r"Father iteration (\d+): son finished, wait returned"
)
# Track which iterations completed
father_calling = set()
son_started = set()
son_delaying = set()
son_finished = set()
wait_returned = set()
def check_output(line: str) -> None:
"""Check log output for expected messages."""
if test_complete.is_set():
return
if mo := father_calling_pattern.search(line):
father_calling.add(int(mo.group(1)))
elif mo := son_started_pattern.search(line):
son_started.add(int(mo.group(1)))
elif mo := son_delaying_pattern.search(line):
son_delaying.add(int(mo.group(1)))
elif mo := son_finished_pattern.search(line):
son_finished.add(int(mo.group(1)))
elif mo := father_wait_returned_pattern.search(line):
iteration = int(mo.group(1))
wait_returned.add(iteration)
# Test completes when iteration 9 finishes
if iteration == 9:
test_complete.set()
# Run with log monitoring
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "test-script-delay-params"
# Get services
_, services = await client.list_entities_services()
test_service = next(
(s for s in services if s.name == "test_repeat_with_delay"), None
)
assert test_service is not None, "test_repeat_with_delay service not found"
# Execute the test
client.execute_service(test_service, {})
# Wait for test to complete (10 iterations * ~100ms each + margin)
try:
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
except TimeoutError:
pytest.fail(
f"Test timed out. Completed iterations: {sorted(wait_returned)}. "
f"This likely indicates the script became a zombie (issue #12044)."
)
# Verify all 10 iterations completed successfully
expected_iterations = set(range(10))
assert father_calling == expected_iterations, "Not all iterations started"
assert son_started == expected_iterations, (
"Son script not started for all iterations"
)
assert son_finished == expected_iterations, (
"Son script not finished for all iterations"
)
assert wait_returned == expected_iterations, (
"script.wait did not return for all iterations"
)
# Verify delays were triggered for iterations >= 5
expected_delays = set(range(5, 10))
assert son_delaying == expected_delays, (
"Delays not triggered for iterations >= 5"
)
@@ -0,0 +1,90 @@
"""Integration test for wait_until FIFO ordering.
This test verifies that when multiple wait_until actions are queued,
they execute in FIFO (First In First Out) order, not LIFO.
PR #7972 introduced a bug where emplace_front() was used, causing
LIFO ordering which is incorrect.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_wait_until_fifo_ordering(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that wait_until executes queued items in FIFO order.
With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO).
With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO).
"""
test_complete = asyncio.Event()
# Track completion order
completed_order = []
# Patterns to match
queuing_pattern = re.compile(r"Queueing iteration (\d+)")
completed_pattern = re.compile(r"Completed iteration (\d+)")
def check_output(line: str) -> None:
"""Check log output for completion order."""
if test_complete.is_set():
return
if mo := queuing_pattern.search(line):
iteration = int(mo.group(1))
elif mo := completed_pattern.search(line):
iteration = int(mo.group(1))
completed_order.append(iteration)
# Test completes when all 5 have completed
if len(completed_order) == 5:
test_complete.set()
# Run with log monitoring
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "test-wait-until-ordering"
# Get services
_, services = await client.list_entities_services()
test_service = next(
(s for s in services if s.name == "test_wait_until_fifo"), None
)
assert test_service is not None, "test_wait_until_fifo service not found"
# Execute the test
client.execute_service(test_service, {})
# Wait for test to complete
try:
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
except TimeoutError:
pytest.fail(
f"Test timed out. Completed order: {completed_order}. "
f"Expected 5 completions but got {len(completed_order)}."
)
# Verify FIFO order
expected_order = [0, 1, 2, 3, 4]
assert completed_order == expected_order, (
f"Unexpected order: {completed_order}. "
f"Expected FIFO order: {expected_order}"
)
+31
View File
@@ -737,6 +737,37 @@ def test_write_cpp_with_duplicate_markers(
write_cpp("// New code")
@patch("esphome.writer.CORE")
def test_clean_all_with_yaml_file(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all with a .yaml file uses parent directory."""
# Create config directory with yaml file
config_dir = tmp_path / "config"
config_dir.mkdir()
yaml_file = config_dir / "test.yaml"
yaml_file.write_text("esphome:\n name: test\n")
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(yaml_file)])
# Verify .esphome directory still exists but contents cleaned
assert build_dir.exists()
assert not (build_dir / "dummy.txt").exists()
# Verify logging mentions the build dir
assert "Cleaning" in caplog.text
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all(
mock_core: MagicMock,