mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
Merge remote-tracking branch 'upstream/dev' into api-proto-max-length
This commit is contained in:
@@ -112,7 +112,9 @@ AGS10_SET_ZERO_POINT_ACTION_MODE = {
|
||||
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AGS10Component),
|
||||
cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True),
|
||||
cv.Required(CONF_MODE): cv.templatable(
|
||||
cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True)
|
||||
),
|
||||
cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t),
|
||||
},
|
||||
)
|
||||
@@ -127,8 +129,10 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
mode = await cg.templatable(config.get(CONF_MODE), args, enumerate)
|
||||
mode = await cg.templatable(
|
||||
config.get(CONF_MODE), args, AGS10SetZeroPointActionMode
|
||||
)
|
||||
cg.add(var.set_mode(mode))
|
||||
value = await cg.templatable(config[CONF_VALUE], args, int)
|
||||
value = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
|
||||
cg.add(var.set_value(value))
|
||||
return var
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
#include "esphome/components/rp2040/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
@@ -276,6 +279,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
rp2040::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
|
||||
@@ -244,132 +244,144 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
|
||||
* traffic.
|
||||
*/
|
||||
// Split into per-state methods so the compiler doesn't allocate stack space
|
||||
// for all branches simultaneously. On RP2040 the core0 stack lives in a 4KB
|
||||
// scratch RAM bank; the Noise crypto path (curve25519) needs ~2KB+ of stack,
|
||||
// so every byte saved in the caller matters.
|
||||
APIError APINoiseFrameHelper::state_action_() {
|
||||
int err;
|
||||
APIError aerr;
|
||||
if (state_ == State::INITIALIZE) {
|
||||
HELPER_LOG("Bad state for method: %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
switch (this->state_) {
|
||||
case State::INITIALIZE:
|
||||
HELPER_LOG("Bad state for method: %d", (int) this->state_);
|
||||
return APIError::BAD_STATE;
|
||||
case State::CLIENT_HELLO:
|
||||
return this->state_action_client_hello_();
|
||||
case State::SERVER_HELLO:
|
||||
return this->state_action_server_hello_();
|
||||
case State::HANDSHAKE:
|
||||
return this->state_action_handshake_();
|
||||
case State::CLOSED:
|
||||
case State::FAILED:
|
||||
return APIError::BAD_STATE;
|
||||
default:
|
||||
return APIError::OK;
|
||||
}
|
||||
if (state_ == State::CLIENT_HELLO) {
|
||||
// waiting for client hello
|
||||
aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
// ignore contents, may be used in future for flags
|
||||
// Resize for: existing prologue + 2 size bytes + frame data
|
||||
size_t old_size = this->prologue_.size();
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
this->prologue_.resize(old_size + 2 + rx_size);
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||
}
|
||||
|
||||
state_ = State::SERVER_HELLO;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_client_hello_() {
|
||||
// waiting for client hello
|
||||
APIError aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
const auto &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.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_ADDRESS_BUFFER_SIZE;
|
||||
|
||||
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
||||
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
||||
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
|
||||
uint8_t msg[max_msg_size];
|
||||
|
||||
// chosen proto
|
||||
msg[0] = 0x01;
|
||||
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||
|
||||
aerr = write_frame_(msg, total_size);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
// ignore contents, may be used in future for flags
|
||||
// Resize for: existing prologue + 2 size bytes + frame data
|
||||
size_t old_size = this->prologue_.size();
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
this->prologue_.resize(old_size + 2 + rx_size);
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||
}
|
||||
if (state_ == State::HANDSHAKE) {
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
// waiting for handshake msg
|
||||
aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
|
||||
if (this->rx_buf_.empty()) {
|
||||
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (this->rx_buf_[0] != 0x00) {
|
||||
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
||||
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
}
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
|
||||
: LOG_STR("Handshake error"));
|
||||
return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
|
||||
aerr = check_handshake_finished_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
uint8_t buffer[65];
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
|
||||
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
|
||||
APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
|
||||
APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||
if (aerr_write != APIError::OK)
|
||||
return aerr_write;
|
||||
buffer[0] = 0x00; // success
|
||||
|
||||
aerr = write_frame_(buffer, mbuf.size + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
aerr = check_handshake_finished_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
} else {
|
||||
// bad state for action
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
}
|
||||
if (state_ == State::CLOSED || state_ == State::FAILED) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
state_ = State::SERVER_HELLO;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
// send server hello
|
||||
const auto &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.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_ADDRESS_BUFFER_SIZE;
|
||||
|
||||
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
||||
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
||||
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
|
||||
uint8_t msg[max_msg_size];
|
||||
|
||||
// chosen proto
|
||||
msg[0] = 0x01;
|
||||
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||
|
||||
APIError aerr = write_frame_(msg, total_size);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_() {
|
||||
int action = noise_handshakestate_get_action(this->handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
return this->state_action_handshake_read_();
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
return this->state_action_handshake_write_();
|
||||
}
|
||||
// bad state for action
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
APIError aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return this->handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
|
||||
if (this->rx_buf_.empty()) {
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (this->rx_buf_[0] != 0x00) {
|
||||
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
}
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
|
||||
: LOG_STR("Handshake error"));
|
||||
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
|
||||
return this->check_handshake_finished_();
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
uint8_t buffer[65];
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
|
||||
APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
buffer[0] = 0x00; // success
|
||||
|
||||
aerr = this->write_frame_(buffer, mbuf.size + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->check_handshake_finished_();
|
||||
}
|
||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
||||
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
|
||||
uint8_t data[32];
|
||||
|
||||
@@ -26,6 +26,11 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
|
||||
protected:
|
||||
APIError state_action_();
|
||||
APIError state_action_client_hello_();
|
||||
APIError state_action_server_hello_();
|
||||
APIError state_action_handshake_();
|
||||
APIError state_action_handshake_read_();
|
||||
APIError state_action_handshake_write_();
|
||||
APIError try_read_frame_();
|
||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||
APIError init_handshake_();
|
||||
|
||||
@@ -16,7 +16,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
|
||||
}
|
||||
|
||||
void Button::press() {
|
||||
ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str());
|
||||
ESP_LOGV(TAG, "'%s' Pressed.", this->get_name().c_str());
|
||||
this->press_action();
|
||||
this->press_callback_.call();
|
||||
}
|
||||
|
||||
@@ -117,8 +117,9 @@ FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
|
||||
|
||||
|
||||
async def setup_display_core_(var, config):
|
||||
if CONF_ROTATION in config:
|
||||
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
|
||||
if rotation := config.get(CONF_ROTATION, 0):
|
||||
# Default initialised value for rotation is 0
|
||||
cg.add(var.set_rotation(DISPLAY_ROTATIONS[rotation]))
|
||||
|
||||
if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
|
||||
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
|
||||
|
||||
@@ -175,9 +175,7 @@ async def to_code(config):
|
||||
*model.get_constructor_args(config),
|
||||
)
|
||||
|
||||
# Rotation is handled by setting the transform
|
||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||
await display.register_display(var, display_config)
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
@@ -201,16 +199,6 @@ async def to_code(config):
|
||||
transform[CONF_SWAP_XY] = False
|
||||
else:
|
||||
transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS}
|
||||
rotation = config[CONF_ROTATION]
|
||||
if rotation == 180:
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
elif rotation == 90:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
elif rotation == 270:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
transform_str = "|".join(
|
||||
{
|
||||
str(getattr(Transform, x.upper()))
|
||||
|
||||
@@ -97,6 +97,23 @@ bool EPaperBase::reset() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperBase::update_effective_transform_() {
|
||||
switch (this->rotation_) {
|
||||
case DISPLAY_ROTATION_90_DEGREES:
|
||||
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X);
|
||||
break;
|
||||
case DISPLAY_ROTATION_180_DEGREES:
|
||||
this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X);
|
||||
break;
|
||||
case DISPLAY_ROTATION_270_DEGREES:
|
||||
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y);
|
||||
break;
|
||||
default:
|
||||
this->effective_transform_ = this->transform_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperBase::update() {
|
||||
if (this->state_ != EPaperState::IDLE) {
|
||||
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
|
||||
@@ -280,11 +297,11 @@ bool EPaperBase::initialise(bool partial) {
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return false;
|
||||
if (this->transform_ & SWAP_XY)
|
||||
if (this->effective_transform_ & SWAP_XY)
|
||||
std::swap(x, y);
|
||||
if (this->transform_ & MIRROR_X)
|
||||
if (this->effective_transform_ & MIRROR_X)
|
||||
x = this->width_ - x - 1;
|
||||
if (this->transform_ & MIRROR_Y)
|
||||
if (this->effective_transform_ & MIRROR_Y)
|
||||
y = this->height_ - y - 1;
|
||||
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/split_buffer/split_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -51,7 +51,14 @@ class EPaperBase : public Display,
|
||||
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
||||
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
||||
void set_transform(uint8_t transform) { this->transform_ = transform; }
|
||||
void set_transform(uint8_t transform) {
|
||||
this->transform_ = transform;
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
void set_rotation(DisplayRotation rotation) override {
|
||||
Display::set_rotation(rotation);
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; }
|
||||
void dump_config() override;
|
||||
|
||||
@@ -106,8 +113,8 @@ class EPaperBase : public Display,
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; };
|
||||
int get_width_internal() override { return this->width_; };
|
||||
int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void process_state_();
|
||||
|
||||
@@ -119,6 +126,7 @@ class EPaperBase : public Display,
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
|
||||
/**
|
||||
@@ -171,6 +179,7 @@ class EPaperBase : public Display,
|
||||
uint32_t delay_until_{}; // timestamp until which to delay processing
|
||||
uint16_t next_delay_{}; // milliseconds to delay before next state
|
||||
uint8_t transform_{};
|
||||
uint8_t effective_transform_{};
|
||||
uint8_t update_count_{};
|
||||
// these values represent the bounds of the updated buffer. Note that x_high and y_high
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
|
||||
@@ -48,7 +48,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
from esphome.writer import clean_cmake_cache
|
||||
from esphome.writer import clean_build, clean_cmake_cache
|
||||
|
||||
from .boards import BOARDS, STANDARD_BOARDS
|
||||
from .const import ( # noqa
|
||||
@@ -97,8 +97,12 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
|
||||
CONF_SIGNING_KEY = "signing_key"
|
||||
CONF_SIGNING_SCHEME = "signing_scheme"
|
||||
CONF_SRAM1_AS_IRAM = "sram1_as_iram"
|
||||
CONF_SUBTYPE = "subtype"
|
||||
CONF_VERIFICATION_KEY = "verification_key"
|
||||
|
||||
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
|
||||
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
|
||||
@@ -120,6 +124,27 @@ ASSERTION_LEVELS = {
|
||||
"SILENT": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT",
|
||||
}
|
||||
|
||||
SIGNING_SCHEMES = {
|
||||
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
|
||||
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
|
||||
}
|
||||
|
||||
# Chip variants that only support one signing scheme for Secure Boot V2.
|
||||
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
|
||||
# Variants not listed in either set support both RSA and ECDSA
|
||||
# (e.g. C5, C6, H2, P4). New variants should be added to the
|
||||
# appropriate set if they only support one scheme.
|
||||
SIGNED_OTA_RSA_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C3,
|
||||
}
|
||||
SIGNED_OTA_ECC_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C61,
|
||||
}
|
||||
|
||||
COMPILER_OPTIMIZATIONS = {
|
||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
|
||||
@@ -962,6 +987,47 @@ def final_validate(config):
|
||||
)
|
||||
# disable the rollback feature anyway since it can't be used.
|
||||
advanced[CONF_ENABLE_OTA_ROLLBACK] = False
|
||||
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
||||
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
||||
variant = config[CONF_VARIANT]
|
||||
scheme_variant_conflicts = {
|
||||
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
|
||||
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
|
||||
}
|
||||
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
|
||||
0
|
||||
]:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Signing scheme '{scheme}' is not supported on "
|
||||
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
|
||||
path=[
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ADVANCED,
|
||||
CONF_SIGNED_OTA_VERIFICATION,
|
||||
CONF_SIGNING_SCHEME,
|
||||
],
|
||||
)
|
||||
)
|
||||
if CONF_OTA not in full_config:
|
||||
_LOGGER.warning(
|
||||
"Signed OTA verification is enabled but no OTA component is configured. "
|
||||
"The initial firmware will be signed but OTA updates won't be possible "
|
||||
"until an OTA component is added."
|
||||
)
|
||||
if CONF_SIGNING_KEY in signed_ota:
|
||||
_LOGGER.info(
|
||||
"Signed OTA verification is enabled. Keep your signing key safe! "
|
||||
"If you lose the signing key, you will NOT be able to OTA update "
|
||||
"devices running firmware signed with this key. "
|
||||
"Without the key, you'll need to reflash via serial."
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Signed OTA verification is configured with a public verification key. "
|
||||
"Binaries will NOT be signed automatically during build. "
|
||||
"You must sign them externally before flashing."
|
||||
)
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
@@ -1173,6 +1239,18 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
min=8192, max=32768
|
||||
),
|
||||
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
|
||||
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SIGNING_KEY): cv.file_,
|
||||
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
|
||||
cv.Optional(
|
||||
CONF_SIGNING_SCHEME, default="rsa3072"
|
||||
): cv.one_of(*SIGNING_SCHEMES, lower=True),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||
): cv.boolean,
|
||||
@@ -1878,6 +1956,32 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
|
||||
cg.add_define("USE_OTA_ROLLBACK")
|
||||
|
||||
# Enable signed app verification without hardware secure boot
|
||||
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT", True)
|
||||
|
||||
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
||||
for key, flag in SIGNING_SCHEMES.items():
|
||||
add_idf_sdkconfig_option(flag, scheme == key)
|
||||
|
||||
if CONF_SIGNING_KEY in signed_ota:
|
||||
# Private key mode — auto-sign binaries during build
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_SIGNING_KEY",
|
||||
str(signed_ota[CONF_SIGNING_KEY].resolve()),
|
||||
)
|
||||
else:
|
||||
# Public key mode — verification only, external signing required
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
str(signed_ota[CONF_VERIFICATION_KEY].resolve()),
|
||||
)
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
|
||||
|
||||
cg.add_define(
|
||||
@@ -2195,6 +2299,7 @@ def _write_sdkconfig():
|
||||
if write_file_if_changed(internal_path, contents):
|
||||
# internal changed, update real one
|
||||
write_file_if_changed(sdk_path, contents)
|
||||
clean_build(clear_pio_cache=False)
|
||||
|
||||
|
||||
def _write_idf_component_yml():
|
||||
|
||||
@@ -8,6 +8,99 @@ import shutil # noqa: E402
|
||||
from glob import glob # noqa: E402
|
||||
|
||||
|
||||
def _parse_sdkconfig(sdkconfig_path):
|
||||
"""Parse sdkconfig file and return a dict of CONFIG_ options."""
|
||||
options = {}
|
||||
try:
|
||||
for line in sdkconfig_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, _, value = line.partition("=")
|
||||
# Strip surrounding quotes from string values
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
options[key] = value
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return options
|
||||
|
||||
|
||||
def sign_firmware(source, target, env):
|
||||
"""
|
||||
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
|
||||
Reads signing configuration from sdkconfig.
|
||||
"""
|
||||
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
|
||||
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
|
||||
pioenv = env.subst("$PIOENV")
|
||||
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
|
||||
|
||||
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT") != "y":
|
||||
return
|
||||
|
||||
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") != "y":
|
||||
print("Signed OTA verification enabled but build-time signing disabled.")
|
||||
print("You must sign the firmware externally before flashing.")
|
||||
return
|
||||
|
||||
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
|
||||
if not signing_key:
|
||||
print("Error: CONFIG_SECURE_BOOT_SIGNING_KEY not set in sdkconfig")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
signing_key_path = pathlib.Path(signing_key)
|
||||
if not signing_key_path.exists():
|
||||
print(f"Error: Signing key not found: {signing_key_path}")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
|
||||
# so the espsecure signature version is always 2.
|
||||
sign_version = "2"
|
||||
|
||||
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
|
||||
firmware_path = build_dir / firmware_name
|
||||
|
||||
if not firmware_path.exists():
|
||||
print(f"Error: Firmware binary not found: {firmware_path}")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
python_exe = f'"{env.subst("$PYTHONEXE")}"'
|
||||
unsigned_path = firmware_path.with_suffix(".unsigned.bin")
|
||||
|
||||
# Keep a copy of the unsigned binary
|
||||
shutil.copyfile(str(firmware_path), str(unsigned_path))
|
||||
|
||||
cmd = [
|
||||
python_exe,
|
||||
"-m",
|
||||
"espsecure",
|
||||
"sign-data",
|
||||
"--version",
|
||||
sign_version,
|
||||
"--keyfile",
|
||||
str(signing_key_path),
|
||||
"--output",
|
||||
str(firmware_path),
|
||||
str(unsigned_path),
|
||||
]
|
||||
|
||||
print(f"Signing firmware with key: {signing_key_path.name}")
|
||||
result = env.Execute(
|
||||
env.VerboseAction(" ".join(cmd), "Signing firmware with espsecure")
|
||||
)
|
||||
|
||||
if result == 0:
|
||||
print("Successfully signed firmware")
|
||||
else:
|
||||
print(f"Error: espsecure sign_data failed with code {result}")
|
||||
# Restore unsigned binary on failure
|
||||
shutil.copyfile(str(unsigned_path), str(firmware_path))
|
||||
env.Exit(1)
|
||||
|
||||
|
||||
def merge_factory_bin(source, target, env):
|
||||
"""
|
||||
Merges all flash sections into a single .factory.bin using esptool.
|
||||
@@ -124,7 +217,8 @@ def esp32_copy_ota_bin(source, target, env):
|
||||
print(f"Copied firmware to {new_file_name}")
|
||||
|
||||
|
||||
# Run merge first, then ota copy second
|
||||
# Run signing first, then merge, then ota copy
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821
|
||||
|
||||
|
||||
@@ -399,8 +399,17 @@ void ESP32BLE::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
if (this->advertising_ != nullptr) {
|
||||
this->advertising_->loop();
|
||||
}
|
||||
#endif
|
||||
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
while (ble_event != nullptr) {
|
||||
if (ble_event == nullptr)
|
||||
return;
|
||||
|
||||
do {
|
||||
switch (ble_event->type_) {
|
||||
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
|
||||
case BLEEvent::GATTS: {
|
||||
@@ -488,15 +497,11 @@ void ESP32BLE::loop() {
|
||||
}
|
||||
// Return the event to the pool
|
||||
this->ble_event_pool_.release(ble_event);
|
||||
ble_event = this->ble_events_.pop();
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
if (this->advertising_ != nullptr) {
|
||||
this->advertising_->loop();
|
||||
}
|
||||
#endif
|
||||
} while ((ble_event = this->ble_events_.pop()) != nullptr);
|
||||
|
||||
// Log dropped events periodically
|
||||
// Log dropped events - only reachable when events were processed.
|
||||
// Drops only occur when the queue is full, and only this loop drains it,
|
||||
// so if pop() returned nullptr above we can skip this check (saves a memw).
|
||||
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
|
||||
|
||||
@@ -233,6 +233,7 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
cg.add_define("USE_ESP8266_CRASH_HANDLER")
|
||||
|
||||
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
|
||||
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
|
||||
// Global reset info struct populated by SDK/Arduino core at boot
|
||||
extern struct rst_info resetInfo;
|
||||
}
|
||||
|
||||
// Xtensa windowed-ABI: bits[31:30] encode call type (CALL0=00, CALL4=01,
|
||||
// CALL8=10, CALL12=11). Mask and force bit 30 to recover the real address.
|
||||
static constexpr uint32_t XTENSA_ADDR_MASK = 0x3FFFFFFF;
|
||||
static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000;
|
||||
|
||||
// ESP8266 memory map boundaries for code regions
|
||||
static constexpr uint32_t IRAM_START = 0x40100000;
|
||||
static constexpr uint32_t IRAM_END = 0x40108000; // 32KB
|
||||
|
||||
// Linker symbols for the actual firmware IROM section.
|
||||
// Using these instead of a conservative upper bound (0x40400000) prevents
|
||||
// false positives from stale stack values beyond the actual flash mapping.
|
||||
extern "C" {
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
|
||||
extern void _irom0_text_start(void);
|
||||
extern void _irom0_text_end(void);
|
||||
// NOLINTEND(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
|
||||
}
|
||||
|
||||
// Check if a value looks like a code address in IRAM or flash-mapped IROM.
|
||||
// IRAM_ATTR as safety net — normally inlined into custom_crash_callback, but
|
||||
// ensures correctness if the compiler ever chooses not to inline.
|
||||
static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
|
||||
uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE;
|
||||
return (addr >= IRAM_START && addr < IRAM_END) ||
|
||||
(addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end);
|
||||
}
|
||||
|
||||
// Recover the actual code address from a windowed-ABI return address on the stack.
|
||||
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
|
||||
|
||||
// RTC user memory layout for crash backtrace data.
|
||||
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
|
||||
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
|
||||
// Store 16 raw candidates, filter to real return addresses at log time.
|
||||
static constexpr uint8_t RTC_CRASH_BASE = 174;
|
||||
static constexpr size_t MAX_BACKTRACE = 16;
|
||||
|
||||
// Magic word packs sentinel, version, and count into one uint32_t:
|
||||
// bits[31:16] = sentinel
|
||||
// bits[15:8] = version
|
||||
// bits[7:0] = backtrace count
|
||||
static constexpr uint8_t CRASH_SENTINEL_BITS = 16;
|
||||
static constexpr uint8_t CRASH_VERSION_BITS = 8;
|
||||
|
||||
static constexpr uint16_t CRASH_SENTINEL_VALUE = 0xDEAD;
|
||||
static constexpr uint8_t CRASH_VERSION_VALUE = 1;
|
||||
|
||||
static constexpr uint32_t CRASH_SENTINEL = static_cast<uint32_t>(CRASH_SENTINEL_VALUE) << CRASH_SENTINEL_BITS;
|
||||
static constexpr uint32_t CRASH_VERSION = static_cast<uint32_t>(CRASH_VERSION_VALUE) << CRASH_VERSION_BITS;
|
||||
static constexpr uint32_t CRASH_SENTINEL_MASK = static_cast<uint32_t>(0xFFFF) << CRASH_SENTINEL_BITS;
|
||||
static constexpr uint32_t CRASH_VERSION_MASK = static_cast<uint32_t>(0xFF) << CRASH_VERSION_BITS;
|
||||
static constexpr uint32_t CRASH_COUNT_MASK = 0xFF;
|
||||
|
||||
// Struct layout: 18 RTC blocks (72 bytes):
|
||||
// [0] = magic (sentinel | version | count)
|
||||
// [1..16] = up to 16 code addresses from stack scanning
|
||||
// [17] = epc1 at crash time (to skip duplicates at log time)
|
||||
struct RtcCrashData {
|
||||
uint32_t magic;
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint32_t epc1; // Fault PC, used to filter duplicates
|
||||
};
|
||||
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
static const char *const TAG = "esp8266";
|
||||
|
||||
static inline bool is_crash_reason(uint32_t reason) {
|
||||
return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST;
|
||||
}
|
||||
|
||||
bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); }
|
||||
|
||||
// Xtensa exception cause names for the LX106 core (ESP8266).
|
||||
// Only includes causes that can actually occur on the LX106 — it has no MMU,
|
||||
// no TLB, no PIF, and no privilege levels, so causes 12-18 and 24-26 are
|
||||
// impossible and omitted. The numeric cause is always logged as fallback.
|
||||
// Uses if-else with LOG_STR to avoid CSWTCH jump tables (RAM on ESP8266).
|
||||
static const LogString *get_exception_cause(uint32_t cause) {
|
||||
if (cause == 0)
|
||||
return LOG_STR("IllegalInst");
|
||||
if (cause == 2)
|
||||
return LOG_STR("InstFetchErr");
|
||||
if (cause == 3)
|
||||
return LOG_STR("LoadStoreErr");
|
||||
if (cause == 4)
|
||||
return LOG_STR("Level1Int");
|
||||
if (cause == 6)
|
||||
return LOG_STR("DivByZero");
|
||||
if (cause == 9)
|
||||
return LOG_STR("Alignment");
|
||||
if (cause == 20)
|
||||
return LOG_STR("InstFetchProhibit");
|
||||
if (cause == 28)
|
||||
return LOG_STR("LoadProhibit");
|
||||
if (cause == 29)
|
||||
return LOG_STR("StoreProhibit");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const LogString *get_reset_reason(uint32_t reason) {
|
||||
if (reason == REASON_WDT_RST)
|
||||
return LOG_STR("Hardware WDT");
|
||||
if (reason == REASON_EXCEPTION_RST)
|
||||
return LOG_STR("Exception");
|
||||
if (reason == REASON_SOFT_WDT_RST)
|
||||
return LOG_STR("Soft WDT");
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
// Read backtrace from RTC user memory into caller-provided buffer.
|
||||
// Returns the number of valid backtrace entries (0 if no data found).
|
||||
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
|
||||
RtcCrashData rtc_data;
|
||||
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
return 0;
|
||||
uint32_t magic = rtc_data.magic;
|
||||
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
|
||||
return 0;
|
||||
uint8_t raw_count = magic & CRASH_COUNT_MASK;
|
||||
if (raw_count > MAX_BACKTRACE)
|
||||
raw_count = MAX_BACKTRACE;
|
||||
// Skip any that match epc1 (already reported as the fault PC).
|
||||
// Note: we cannot verify CALL instructions at addr-3 on ESP8266 because
|
||||
// reading from IROM causes LoadStoreError due to flash cache conflicts
|
||||
// (the reading code and target can share a direct-mapped cache line).
|
||||
// The linker-symbol IROM bounds already eliminate most false positives.
|
||||
uint8_t out = 0;
|
||||
for (uint8_t i = 0; i < raw_count && out < max_entries; i++) {
|
||||
uint32_t addr = rtc_data.backtrace[i];
|
||||
if (addr != rtc_data.epc1)
|
||||
backtrace[out++] = addr;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Intentionally uses separate ESP_LOGE calls per line instead of combining into
|
||||
// one multi-line log message. This ensures each address appears as its own line
|
||||
// on the serial console, making it possible to see partial output if the device
|
||||
// crashes again during boot, and allowing the CLI's process_stacktrace to match
|
||||
// and decode each address individually.
|
||||
void crash_handler_log() {
|
||||
if (!is_crash_reason(resetInfo.reason))
|
||||
return;
|
||||
|
||||
// Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost).
|
||||
// Both resetInfo and RTC data survive until the next reset, so this can be
|
||||
// called multiple times (logger init + API subscribe) with the same result.
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
|
||||
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
|
||||
// the Arduino core's postmortem handler behavior.
|
||||
static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0;
|
||||
static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6;
|
||||
static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5;
|
||||
static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d;
|
||||
uint32_t exccause = resetInfo.exccause;
|
||||
if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION &&
|
||||
(resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) {
|
||||
exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO;
|
||||
}
|
||||
const LogString *cause = get_exception_cause(exccause);
|
||||
if (cause != nullptr) {
|
||||
ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)),
|
||||
LOG_STR_ARG(cause), exccause);
|
||||
} else {
|
||||
ESP_LOGE(TAG, " Reason: %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), exccause);
|
||||
}
|
||||
ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1);
|
||||
if (resetInfo.reason == REASON_EXCEPTION_RST) {
|
||||
ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32, resetInfo.excvaddr);
|
||||
}
|
||||
for (uint8_t i = 0; i < bt_count; i++) {
|
||||
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
// --- Custom crash callback ---
|
||||
// Overrides the weak custom_crash_callback() from Arduino core's
|
||||
// core_esp8266_postmortem.cpp. Called during exception handling before
|
||||
// the device restarts. We scan the full stack for code addresses and store
|
||||
// them in RTC user memory (which survives software reset).
|
||||
extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint32_t stack, uint32_t stack_end) {
|
||||
// No zero-init — only magic, epc1, and backtrace[0..count-1] are read.
|
||||
// Saves the IRAM cost of a 72-byte zero-init loop.
|
||||
RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init)
|
||||
uint8_t count = 0;
|
||||
|
||||
// Stack pointer from the Xtensa exception frame is always 4-byte aligned.
|
||||
auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr)
|
||||
auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr)
|
||||
uint32_t epc1 = rst_info->epc1;
|
||||
|
||||
for (; scan < end && count < MAX_BACKTRACE; scan++) {
|
||||
uint32_t val = *scan;
|
||||
if (is_code_addr(val)) {
|
||||
uint32_t addr = recover_code_addr(val);
|
||||
// Skip epc1 — already reported as the fault PC
|
||||
if (addr != epc1)
|
||||
data.backtrace[count++] = addr;
|
||||
}
|
||||
}
|
||||
|
||||
data.epc1 = epc1;
|
||||
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
|
||||
|
||||
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
|
||||
}
|
||||
|
||||
#endif // USE_ESP8266_CRASH_HANDLER
|
||||
#endif // USE_ESP8266
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
/// Returns true if the previous boot was a crash (exception, WDT, or soft WDT).
|
||||
bool crash_handler_has_data();
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
#endif // USE_ESP8266_CRASH_HANDLER
|
||||
#endif // USE_ESP8266
|
||||
@@ -19,12 +19,13 @@ static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
||||
|
||||
// RTC memory layout for preferences:
|
||||
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127)
|
||||
// - Normal region: RTC words 32-127 (mapped from preference offset 0-95)
|
||||
// RTC memory layout:
|
||||
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
|
||||
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
|
||||
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs
|
||||
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
|
||||
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
|
||||
|
||||
// Maximum preference size in words (limited by uint8_t length_words field)
|
||||
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
||||
|
||||
@@ -104,6 +104,8 @@ CONF_CLK_MODE = "clk_mode"
|
||||
CONF_POWER_PIN = "power_pin"
|
||||
CONF_PHY_REGISTERS = "phy_registers"
|
||||
|
||||
CONF_INTERFACE = "interface"
|
||||
|
||||
CONF_CLOCK_SPEED = "clock_speed"
|
||||
|
||||
EthernetType = ethernet_ns.enum("EthernetType")
|
||||
@@ -191,6 +193,13 @@ CLK_MODES_DEPRECATED = {
|
||||
"GPIO17_OUT": ("CLK_OUT", 17),
|
||||
}
|
||||
|
||||
spi_host_device_t = cg.global_ns.enum("spi_host_device_t")
|
||||
|
||||
SPI_INTERFACE_MAP = {
|
||||
"spi2": spi_host_device_t.SPI2_HOST,
|
||||
"spi3": spi_host_device_t.SPI3_HOST,
|
||||
}
|
||||
|
||||
MANUAL_IP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4address,
|
||||
@@ -225,6 +234,24 @@ def _is_framework_spi_polling_mode_supported() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _validate_spi_interface(config: ConfigType) -> ConfigType:
|
||||
"""Set default SPI interface or validate user choice against the variant."""
|
||||
if not CORE.is_esp32:
|
||||
return config
|
||||
from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant
|
||||
from esphome.components.spi import get_hw_interface_list
|
||||
|
||||
has_spi3 = "spi3" in sum(get_hw_interface_list(), [])
|
||||
if CONF_INTERFACE not in config:
|
||||
# Only classic ESP32 defaults to spi3; all others default to spi2
|
||||
config[CONF_INTERFACE] = (
|
||||
"spi3" if get_esp32_variant() == VARIANT_ESP32 else "spi2"
|
||||
)
|
||||
elif config[CONF_INTERFACE] == "spi3" and not has_spi3:
|
||||
raise cv.Invalid("Interface 'spi3' is not available on this variant.")
|
||||
return config
|
||||
|
||||
|
||||
def _validate(config):
|
||||
if CONF_USE_ADDRESS not in config:
|
||||
if CONF_MANUAL_IP in config:
|
||||
@@ -368,6 +395,10 @@ SPI_SCHEMA = cv.All(
|
||||
cv.frequency,
|
||||
cv.int_range(int(8e6), int(80e6)),
|
||||
),
|
||||
cv.Optional(CONF_INTERFACE): cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.one_of(*SPI_INTERFACE_MAP.keys(), lower=True),
|
||||
),
|
||||
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
|
||||
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
|
||||
cv.only_on_esp32,
|
||||
@@ -378,6 +409,7 @@ SPI_SCHEMA = cv.All(
|
||||
),
|
||||
),
|
||||
cv.only_on([Platform.ESP32, Platform.RP2040]),
|
||||
_validate_spi_interface,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -408,37 +440,18 @@ def _final_validate_spi(config):
|
||||
return # SPI interface validation is ESP32-only
|
||||
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
|
||||
return
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
|
||||
if spi_configs := fv.full_config.get().get(CONF_SPI):
|
||||
variant = get_esp32_variant()
|
||||
if variant in (
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
):
|
||||
spi_host = "SPI2_HOST"
|
||||
else:
|
||||
spi_host = "SPI3_HOST"
|
||||
# get_spi_interface() returns strings like "SPI2_HOST"
|
||||
spi_host = f"{config[CONF_INTERFACE].upper()}_HOST"
|
||||
for spi_conf in spi_configs:
|
||||
if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None:
|
||||
interface = get_spi_interface(index)
|
||||
if interface == spi_host:
|
||||
raise cv.Invalid(
|
||||
f"`spi` component is using interface '{interface}'. "
|
||||
f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.",
|
||||
f"The `ethernet` and `spi` components are both using interface '{interface}'. "
|
||||
f"To use {config[CONF_TYPE]}, change the `interface` on either `ethernet:` or `spi:`."
|
||||
)
|
||||
|
||||
|
||||
@@ -528,6 +541,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
|
||||
|
||||
cg.add_define("USE_ETHERNET_SPI")
|
||||
|
||||
cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]))
|
||||
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
|
||||
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
|
||||
# ENC28J60 was never built-in to IDF, so it has no Kconfig option
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "esp_eth.h"
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#include "hal/spi_types.h"
|
||||
#endif
|
||||
#include "esp_eth_mac.h"
|
||||
#include "esp_eth_mac_esp.h"
|
||||
#include "esp_netif.h"
|
||||
@@ -135,6 +138,7 @@ class EthernetComponent final : public Component {
|
||||
void set_interrupt_pin(uint8_t interrupt_pin);
|
||||
void set_reset_pin(uint8_t reset_pin);
|
||||
void set_clock_speed(int clock_speed);
|
||||
void set_interface(spi_host_device_t interface);
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void set_polling_interval(uint32_t polling_interval);
|
||||
#endif
|
||||
@@ -201,6 +205,7 @@ class EthernetComponent final : public Component {
|
||||
int reset_pin_{-1};
|
||||
int phy_addr_spi_{-1};
|
||||
int clock_speed_;
|
||||
spi_host_device_t interface_{SPI3_HOST};
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -158,12 +158,7 @@ void EthernetComponent::setup() {
|
||||
.intr_flags = 0,
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
auto host = SPI2_HOST;
|
||||
#else
|
||||
auto host = SPI3_HOST;
|
||||
#endif
|
||||
auto host = this->interface_;
|
||||
|
||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||
@@ -458,6 +453,11 @@ void EthernetComponent::dump_config() {
|
||||
" MOSI Pin: %u\n"
|
||||
" CS Pin: %u",
|
||||
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
|
||||
const char *spi_interface = "spi3";
|
||||
if (this->interface_ == SPI2_HOST) {
|
||||
spi_interface = "spi2";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface);
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
if (this->polling_interval_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_);
|
||||
@@ -760,6 +760,7 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
|
||||
@@ -28,7 +28,10 @@ namespace esphome::gpio_expander {
|
||||
template<typename T, uint16_t N, typename P = typename std::conditional<(N > 256), uint16_t, uint8_t>::type>
|
||||
class CachedGpioExpander {
|
||||
public:
|
||||
/// @brief Read the state of the given pin. This will invalidate the cache for the given pin number.
|
||||
/// @brief Read the state of the given pin.
|
||||
/// By default, each read invalidates the pin's cache entry so the next read
|
||||
/// of the same pin triggers a fresh hardware read. When invalidate_on_read
|
||||
/// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_().
|
||||
/// @param pin Pin number to read
|
||||
/// @return Pin state
|
||||
bool digital_read(P pin) {
|
||||
@@ -36,14 +39,17 @@ class CachedGpioExpander {
|
||||
const T pin_mask = (1 << (pin % BANK_SIZE));
|
||||
// Check if specific pin cache is valid
|
||||
if (this->read_cache_valid_[bank] & pin_mask) {
|
||||
// Invalidate pin
|
||||
this->read_cache_valid_[bank] &= ~pin_mask;
|
||||
if (this->invalidate_on_read_) {
|
||||
// Invalidate pin so next read triggers hardware read
|
||||
this->read_cache_valid_[bank] &= ~pin_mask;
|
||||
}
|
||||
} else {
|
||||
// Read whole bank from hardware
|
||||
if (!this->digital_read_hw(pin))
|
||||
return false;
|
||||
// Mark bank cache as valid except the pin that is being returned now
|
||||
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~pin_mask;
|
||||
// (when not invalidating on read, mark all pins including this one as valid)
|
||||
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~(this->invalidate_on_read_ ? pin_mask : 0);
|
||||
}
|
||||
return this->digital_read_cache(pin);
|
||||
}
|
||||
@@ -71,12 +77,18 @@ class CachedGpioExpander {
|
||||
/// @brief Invalidate cache. This function should be called in component loop().
|
||||
void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
|
||||
|
||||
/// @brief Control whether digital_read() invalidates the pin's cache entry after reading.
|
||||
/// When enabled (default), each read self-invalidates so the next read triggers a hardware read.
|
||||
/// When disabled, cache stays valid until reset_pin_cache_() is explicitly called.
|
||||
void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; }
|
||||
|
||||
static constexpr uint16_t BITS_PER_BYTE = 8;
|
||||
static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
|
||||
static constexpr size_t BANKS = N / BANK_SIZE;
|
||||
static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
|
||||
|
||||
T read_cache_valid_[BANKS]{0};
|
||||
bool invalidate_on_read_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::gpio_expander
|
||||
|
||||
@@ -73,13 +73,13 @@ void HLW8012Component::update() {
|
||||
// Only read cf1 after one cycle. Apparently it's quite unstable after being changed.
|
||||
if (this->current_mode_) {
|
||||
float current = cf1_hz * this->current_multiplier_;
|
||||
ESP_LOGD(TAG, "Got power=%.1fW, current=%.1fA", power, current);
|
||||
ESP_LOGV(TAG, "Got power=%.1fW, current=%.1fA", power, current);
|
||||
if (this->current_sensor_ != nullptr) {
|
||||
this->current_sensor_->publish_state(current);
|
||||
}
|
||||
} else {
|
||||
float voltage = cf1_hz * this->voltage_multiplier_;
|
||||
ESP_LOGD(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage);
|
||||
ESP_LOGV(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage);
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
DEPRECATED_COMPONENT = """
|
||||
The 'ili9xxx' component is deprecated and no new models will be added to it.
|
||||
New model PRs should target the newer and more performant 'mipi_spi' component.
|
||||
"""
|
||||
|
||||
@@ -219,6 +219,9 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
LOGGER.warning(
|
||||
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||
)
|
||||
rhs = MODELS[config[CONF_MODEL]].new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifdef USE_LN882X
|
||||
|
||||
#include "internal_temperature.h"
|
||||
|
||||
extern "C" {
|
||||
uint16_t hal_adc_get_data(uint32_t adc_base, uint32_t ch);
|
||||
}
|
||||
|
||||
namespace esphome::internal_temperature {
|
||||
|
||||
void InternalTemperatureSensor::update() {
|
||||
static constexpr uint32_t ADC_BASE = 0x40000800U;
|
||||
static constexpr uint32_t ADC_CH0 = 1U;
|
||||
static constexpr uint16_t ADC_MASK = 0xFFF;
|
||||
static constexpr float ADC_TEMP_SCALE = 2.54f;
|
||||
static constexpr float ADC_TEMP_OFFSET = 278.15f;
|
||||
uint16_t raw = hal_adc_get_data(ADC_BASE, ADC_CH0);
|
||||
float temperature = (raw & ADC_MASK) / ADC_TEMP_SCALE - ADC_TEMP_OFFSET;
|
||||
this->publish_state(temperature);
|
||||
}
|
||||
|
||||
} // namespace esphome::internal_temperature
|
||||
|
||||
#endif // USE_LN882X
|
||||
@@ -8,6 +8,7 @@ from esphome.const import (
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2040,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -30,7 +31,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
).extend(cv.polling_component_schema("60s")),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_NRF52]),
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_LN882X,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +62,9 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
"internal_temperature_bk72xx.cpp": {
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
},
|
||||
"internal_temperature_ln882x.cpp": {
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"internal_temperature_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include "logger.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
@@ -26,6 +30,9 @@ void Logger::pre_setup() {
|
||||
global_logger = this;
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
|
||||
@@ -392,6 +392,9 @@ async def to_code(configs):
|
||||
} & styles_used:
|
||||
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
|
||||
if configs[0].get(df.CONF_THEME, {}).get(df.CONF_DARK_MODE):
|
||||
df.add_define("LV_THEME_DEFAULT_DARK", "1")
|
||||
|
||||
# Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending
|
||||
lv_image_formats = {"RGB565", "ARGB8888"}
|
||||
if {
|
||||
@@ -459,8 +462,11 @@ def add_hello_world(config):
|
||||
def _theme_schema(value):
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
cv.Optional(df.CONF_DARK_MODE, default=False): cv.boolean,
|
||||
**{
|
||||
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
},
|
||||
}
|
||||
)(value)
|
||||
|
||||
|
||||
@@ -598,6 +598,7 @@ CONF_FLEX_ALIGN_CROSS = "flex_align_cross"
|
||||
CONF_FLEX_ALIGN_TRACK = "flex_align_track"
|
||||
CONF_FLEX_GROW = "flex_grow"
|
||||
CONF_FREEZE = "freeze"
|
||||
CONF_DARK_MODE = "dark_mode"
|
||||
CONF_FULL_REFRESH = "full_refresh"
|
||||
CONF_GRADIENTS = "gradients"
|
||||
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- obj:
|
||||
id: hello_world_card_
|
||||
pad_all: 12
|
||||
bg_color: white
|
||||
bg_opa: cover
|
||||
height: 100%
|
||||
width: 100%
|
||||
scrollable: false
|
||||
|
||||
@@ -7,7 +7,7 @@ from esphome.core import ID
|
||||
from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal
|
||||
from .helpers import add_lv_use
|
||||
from .lvcode import LambdaContext, lv
|
||||
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property
|
||||
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property
|
||||
from .types import ObjUpdateAction, lv_style_t
|
||||
from .widgets import collect_parts, theme_widget_map, wait_for_widgets
|
||||
|
||||
@@ -85,7 +85,7 @@ async def style_update_to_code(config, action_id, template_arg, args):
|
||||
async def theme_to_code(config):
|
||||
if theme := config.get(CONF_THEME):
|
||||
add_lv_use(CONF_THEME)
|
||||
for w_name, style in theme.items():
|
||||
for w_name, style in ((k, v) for k, v in theme.items() if k in WIDGET_TYPES):
|
||||
# Work around Python 3.10 bug with nested async comprehensions
|
||||
# With Python 3.11 this could be simplified
|
||||
# TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions
|
||||
|
||||
@@ -22,9 +22,14 @@ void MCP23008::setup() {
|
||||
// enable open-drain interrupt pins, 3.3V-safe
|
||||
this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR);
|
||||
}
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
}
|
||||
|
||||
void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); }
|
||||
void MCP23008::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MCP23008:");
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
}
|
||||
|
||||
bool MCP23008::read_reg(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace mcp23017 {
|
||||
|
||||
static const char *const TAG = "mcp23017";
|
||||
|
||||
static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin
|
||||
static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins
|
||||
static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin
|
||||
|
||||
void MCP23017::setup() {
|
||||
uint8_t iocon;
|
||||
@@ -19,14 +20,26 @@ void MCP23017::setup() {
|
||||
this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_);
|
||||
this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_);
|
||||
|
||||
uint8_t iocon_flags = 0;
|
||||
if (this->open_drain_ints_) {
|
||||
// enable open-drain interrupt pins, 3.3V-safe
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR);
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR);
|
||||
iocon_flags |= IOCON_ODR;
|
||||
}
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// Mirror INTA/INTB so either pin fires for changes on any port
|
||||
iocon_flags |= IOCON_MIRROR;
|
||||
}
|
||||
if (iocon_flags != 0) {
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | iocon_flags);
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | iocon_flags);
|
||||
}
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
}
|
||||
|
||||
void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); }
|
||||
void MCP23017::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MCP23017:");
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
}
|
||||
|
||||
bool MCP23017::read_reg(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
|
||||
@@ -34,11 +34,14 @@ void MCP23S08::setup() {
|
||||
// enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip)
|
||||
this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR);
|
||||
}
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
}
|
||||
|
||||
void MCP23S08::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MCP23S08:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
}
|
||||
|
||||
bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) {
|
||||
|
||||
@@ -7,9 +7,10 @@ namespace mcp23s17 {
|
||||
static const char *const TAG = "mcp23s17";
|
||||
|
||||
// IOCON register bits
|
||||
static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode
|
||||
static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable
|
||||
static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin
|
||||
static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode
|
||||
static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins
|
||||
static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable
|
||||
static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin
|
||||
|
||||
void MCP23S17::set_device_address(uint8_t device_addr) {
|
||||
if (device_addr != 0) {
|
||||
@@ -37,16 +38,26 @@ void MCP23S17::setup() {
|
||||
this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_);
|
||||
this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_);
|
||||
|
||||
uint8_t iocon_flags = IOCON_SEQOP | IOCON_HAEN;
|
||||
if (this->open_drain_ints_) {
|
||||
// enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip)
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR);
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR);
|
||||
iocon_flags |= IOCON_ODR;
|
||||
}
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// Mirror INTA/INTB so either pin fires for changes on any port
|
||||
iocon_flags |= IOCON_MIRROR;
|
||||
}
|
||||
if (this->open_drain_ints_ || this->interrupt_pin_ != nullptr) {
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon_flags);
|
||||
this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon_flags);
|
||||
}
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
}
|
||||
|
||||
void MCP23S17::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MCP23S17:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
}
|
||||
|
||||
bool MCP23S17::read_reg(uint8_t reg, uint8_t *value) {
|
||||
|
||||
@@ -32,6 +32,16 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_reg(pin, false, iodir);
|
||||
}
|
||||
// When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins
|
||||
// so the chip's INT output fires on any input state change
|
||||
if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
|
||||
}
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->enable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) {
|
||||
|
||||
@@ -44,6 +44,16 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_reg(pin, false, iodir);
|
||||
}
|
||||
// When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins
|
||||
// so the chip's INT output fires on any input state change
|
||||
if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
|
||||
}
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->enable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) {
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -32,6 +33,7 @@ MCP23XXX_INTERRUPT_MODES = {
|
||||
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -43,6 +45,8 @@ async def register_mcp23xxx(config, num_pins):
|
||||
await cg.register_component(var, config)
|
||||
CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins
|
||||
cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT]))
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@ namespace mcp23xxx_base {
|
||||
|
||||
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
|
||||
this->pin_mode(flags_);
|
||||
this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_);
|
||||
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
|
||||
// interrupt for input pins, so skip the explicit call if the user didn't
|
||||
// override the default (NO_INTERRUPT)
|
||||
if (this->interrupt_mode_ != MCP23XXX_NO_INTERRUPT || this->parent_->get_interrupt_pin() == nullptr) {
|
||||
this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_);
|
||||
}
|
||||
}
|
||||
template<uint8_t N> void MCP23XXXGPIOPin<N>::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
template<uint8_t N> bool MCP23XXXGPIOPin<N>::digital_read() {
|
||||
|
||||
@@ -15,11 +15,34 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
|
||||
virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode);
|
||||
|
||||
void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
InternalGPIOPin *get_interrupt_pin() const { return this->interrupt_pin_; }
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
|
||||
void loop() override { this->reset_pin_cache_(); }
|
||||
void loop() override {
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// No need to clear latched interrupts before attaching the ISR — if INT is
|
||||
// already low the ISR fires immediately, loop runs, cache invalidates, and
|
||||
// the GPIO read clears the latch. One harmless extra read at most.
|
||||
void setup_interrupt_pin_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
// read a given register
|
||||
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
|
||||
// write a value to a given register
|
||||
@@ -28,6 +51,7 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
|
||||
virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0;
|
||||
|
||||
bool open_drain_ints_;
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin {
|
||||
|
||||
@@ -279,6 +279,10 @@ def _final_validate(config):
|
||||
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if config[CONF_BUS_MODE] == TYPE_SINGLE:
|
||||
spi.final_validate_device_schema(DOMAIN, require_miso=False, require_mosi=True)(
|
||||
config
|
||||
)
|
||||
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
@@ -286,7 +290,7 @@ def _final_validate(config):
|
||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
||||
# If PSRAM is not enabled, choose a small buffer size by default
|
||||
if not requires_buffer(config):
|
||||
return config # No buffer needed, so no need to set a buffer size
|
||||
return # No need to pick a size
|
||||
color_depth = get_color_depth(config)
|
||||
frac = denominator(config)
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
@@ -298,8 +302,6 @@ def _final_validate(config):
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@@ -1,7 +1,331 @@
|
||||
#include <array>
|
||||
#include <numeric>
|
||||
#include "mitsubishi_cn105.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
static const char *const TAG = "mitsubishi_cn105.driver";
|
||||
|
||||
static constexpr uint32_t WRITE_TIMEOUT_MS = 2000;
|
||||
|
||||
static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10;
|
||||
static constexpr size_t HEADER_LEN = 5;
|
||||
static constexpr uint8_t PREAMBLE = 0xFC;
|
||||
static constexpr uint8_t HEADER_BYTE_1 = 0x01;
|
||||
static constexpr uint8_t HEADER_BYTE_2 = 0x30;
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
|
||||
static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
|
||||
static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01};
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
|
||||
static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
|
||||
static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
|
||||
static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
|
||||
static constexpr std::array<uint8_t, 2> STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP};
|
||||
|
||||
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
|
||||
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
|
||||
}
|
||||
|
||||
template<std::size_t PayloadSize>
|
||||
static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
|
||||
const size_t full_len = PayloadSize + HEADER_LEN + 1;
|
||||
std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
|
||||
std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
|
||||
packet.back() = checksum(packet.data(), packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static float decode_temperature(int temp_a, int temp_b, int delta) {
|
||||
return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
|
||||
}
|
||||
|
||||
static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
|
||||
|
||||
void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); }
|
||||
|
||||
bool MitsubishiCN105::update() {
|
||||
if (const auto start = this->status_update_start_ms_;
|
||||
start && (get_loop_time_ms() - *start) >= this->update_interval_ms_) {
|
||||
this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
this->read_pos_ = 0;
|
||||
this->set_state_(State::READ_TIMEOUT);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->read_incoming_bytes_();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::set_state_(State new_state) {
|
||||
if (should_transition(this->state_, new_state)) {
|
||||
ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
|
||||
LOG_STR_ARG(state_to_string(new_state)));
|
||||
this->state_ = new_state;
|
||||
this->did_transition_(new_state);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
|
||||
LOG_STR_ARG(state_to_string(new_state)));
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::should_transition(State from, State to) {
|
||||
switch (to) {
|
||||
case State::CONNECTING:
|
||||
return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
|
||||
|
||||
case State::CONNECTED:
|
||||
return from == State::CONNECTING;
|
||||
|
||||
case State::UPDATING_STATUS:
|
||||
return from == State::CONNECTED || from == State::STATUS_UPDATED ||
|
||||
from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
|
||||
|
||||
case State::STATUS_UPDATED:
|
||||
return from == State::UPDATING_STATUS;
|
||||
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
return from == State::STATUS_UPDATED;
|
||||
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
return from == State::SCHEDULE_NEXT_STATUS_UPDATE;
|
||||
|
||||
case State::READ_TIMEOUT:
|
||||
return from == State::UPDATING_STATUS || from == State::CONNECTING;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void MitsubishiCN105::did_transition_(State to) {
|
||||
switch (to) {
|
||||
case State::CONNECTING:
|
||||
this->send_packet_(CONNECT_PACKET);
|
||||
break;
|
||||
|
||||
case State::CONNECTED:
|
||||
this->write_timeout_start_ms_.reset();
|
||||
this->status_msg_index_ = 0;
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
break;
|
||||
|
||||
case State::UPDATING_STATUS:
|
||||
this->update_status_();
|
||||
break;
|
||||
|
||||
case State::STATUS_UPDATED: {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) {
|
||||
this->status_msg_index_ = 0;
|
||||
}
|
||||
if (this->status_msg_index_ != 0) {
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
} else {
|
||||
this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
this->status_update_start_ms_ = get_loop_time_ms();
|
||||
this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
break;
|
||||
|
||||
case State::READ_TIMEOUT:
|
||||
this->set_state_(State::CONNECTING);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
|
||||
dump_buffer_vv("TX", packet, len);
|
||||
this->device_.write_array(packet, len);
|
||||
this->write_timeout_start_ms_ = get_loop_time_ms();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::update_status_() {
|
||||
ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_);
|
||||
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {STATUS_MSG_TYPES[this->status_msg_index_]};
|
||||
this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
|
||||
}
|
||||
|
||||
void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) {
|
||||
this->status_update_start_ms_.reset();
|
||||
this->set_state_(state);
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::read_incoming_bytes_() {
|
||||
uint8_t watchdog = 64;
|
||||
while (this->device_.available() > 0 && watchdog-- > 0) {
|
||||
uint8_t &value = this->read_buffer_[this->read_pos_];
|
||||
if (!this->device_.read_byte(&value)) {
|
||||
ESP_LOGW(TAG, "UART read failed while data available");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (++this->read_pos_) {
|
||||
case 1:
|
||||
if (value != PREAMBLE) {
|
||||
this->reset_read_position_and_dump_buffer_("RX ignoring preamble");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 2:
|
||||
continue;
|
||||
|
||||
case 3:
|
||||
if (value != HEADER_BYTE_1) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 4:
|
||||
if (value != HEADER_BYTE_2) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case HEADER_LEN:
|
||||
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
|
||||
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: payload too large");
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
|
||||
if (this->read_pos_ <= len_without_checksum) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checksum(this->read_buffer_, len_without_checksum) != value) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch");
|
||||
continue;
|
||||
}
|
||||
|
||||
bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN,
|
||||
len_without_checksum - HEADER_LEN);
|
||||
this->reset_read_position_and_dump_buffer_("RX");
|
||||
return processed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
|
||||
switch (type) {
|
||||
case PACKET_TYPE_CONNECT_RESPONSE:
|
||||
this->set_state_(State::CONNECTED);
|
||||
return false;
|
||||
|
||||
case PACKET_TYPE_STATUS_RESPONSE:
|
||||
return this->process_status_packet_(payload, len);
|
||||
|
||||
default:
|
||||
ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
|
||||
if (len == 0) {
|
||||
ESP_LOGVV(TAG, "RX status packet too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto previous = this->status_;
|
||||
const auto msg_type = payload[0];
|
||||
if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) {
|
||||
this->set_state_(State::STATUS_UPDATED);
|
||||
}
|
||||
|
||||
return previous != this->status_ && this->is_status_initialized();
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
|
||||
switch (msg_type) {
|
||||
case STATUS_MSG_SETTINGS:
|
||||
return this->parse_status_settings_(payload, len);
|
||||
|
||||
case STATUS_MSG_ROOM_TEMP:
|
||||
return this->parse_status_room_temperature_(payload, len);
|
||||
|
||||
default:
|
||||
ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) {
|
||||
if (len <= 10) {
|
||||
ESP_LOGVV(TAG, "RX settings payload too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->status_.power_on = payload[2] != 0;
|
||||
this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) {
|
||||
if (len <= 5) {
|
||||
ESP_LOGVV(TAG, "RX room temperature payload too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) {
|
||||
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
|
||||
this->read_pos_ = 0;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
|
||||
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||
switch (state) {
|
||||
case State::NOT_CONNECTED:
|
||||
return LOG_STR("Not connected");
|
||||
case State::CONNECTING:
|
||||
return LOG_STR("Connecting");
|
||||
case State::CONNECTED:
|
||||
return LOG_STR("Connected");
|
||||
case State::UPDATING_STATUS:
|
||||
return LOG_STR("UpdatingStatus");
|
||||
case State::STATUS_UPDATED:
|
||||
return LOG_STR("StatusUpdated");
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
return LOG_STR("ScheduleNextStatusUpdate");
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
return LOG_STR("WaitingForScheduledStatusUpdate");
|
||||
case State::READ_TIMEOUT:
|
||||
return LOG_STR("ReadTimeout");
|
||||
}
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -1,19 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t get_loop_time_ms();
|
||||
|
||||
class MitsubishiCN105 {
|
||||
public:
|
||||
struct Status {
|
||||
bool operator==(const Status &) const = default;
|
||||
|
||||
bool power_on{false};
|
||||
float target_temperature{NAN};
|
||||
float room_temperature{NAN};
|
||||
};
|
||||
|
||||
explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {}
|
||||
|
||||
void initialize();
|
||||
bool update();
|
||||
|
||||
uint32_t get_update_interval() const { return this->update_interval_ms_; }
|
||||
void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; }
|
||||
|
||||
const Status &status() const { return this->status_; }
|
||||
bool is_status_initialized() const { return !std::isnan(status_.room_temperature); }
|
||||
|
||||
protected:
|
||||
enum class State : uint8_t {
|
||||
NOT_CONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
UPDATING_STATUS,
|
||||
STATUS_UPDATED,
|
||||
SCHEDULE_NEXT_STATUS_UPDATE,
|
||||
WAITING_FOR_SCHEDULED_STATUS_UPDATE,
|
||||
READ_TIMEOUT
|
||||
};
|
||||
|
||||
void set_state_(State new_state);
|
||||
void did_transition_(State to);
|
||||
bool read_incoming_bytes_();
|
||||
bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
|
||||
bool process_status_packet_(const uint8_t *payload, size_t len);
|
||||
bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len);
|
||||
bool parse_status_settings_(const uint8_t *payload, size_t len);
|
||||
bool parse_status_room_temperature_(const uint8_t *payload, size_t len);
|
||||
void reset_read_position_and_dump_buffer_(const char *prefix);
|
||||
void send_packet_(const uint8_t *packet, size_t len);
|
||||
void update_status_();
|
||||
void cancel_waiting_and_transition_to_(State state);
|
||||
template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); }
|
||||
static bool should_transition(State from, State to);
|
||||
static const LogString *state_to_string(State state);
|
||||
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
|
||||
|
||||
uart::UARTDevice &device_;
|
||||
uint32_t update_interval_ms_{1000};
|
||||
std::optional<uint32_t> write_timeout_start_ms_;
|
||||
std::optional<uint32_t> status_update_start_ms_;
|
||||
Status status_{};
|
||||
State state_{State::NOT_CONNECTED};
|
||||
uint8_t status_msg_index_{0};
|
||||
|
||||
private:
|
||||
static constexpr size_t READ_BUFFER_SIZE = 32;
|
||||
uint8_t read_buffer_[READ_BUFFER_SIZE];
|
||||
uint8_t read_pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <cinttypes>
|
||||
#include "mitsubishi_cn105_climate.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -14,15 +15,36 @@ void MitsubishiCN105Climate::dump_config() {
|
||||
LOG_STR_ARG(parity_to_str(this->parent_->get_parity())), this->parent_->get_stop_bits());
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::setup() {}
|
||||
void MitsubishiCN105Climate::setup() { this->hp_.initialize(); }
|
||||
|
||||
void MitsubishiCN105Climate::loop() {}
|
||||
void MitsubishiCN105Climate::loop() {
|
||||
if (this->hp_.update()) {
|
||||
this->apply_values_();
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {}
|
||||
|
||||
void MitsubishiCN105Climate::apply_values_() {
|
||||
const auto &status = this->hp_.status();
|
||||
|
||||
this->target_temperature = status.target_temperature;
|
||||
this->current_temperature = status.room_temperature;
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -21,6 +21,8 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
|
||||
void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); }
|
||||
|
||||
protected:
|
||||
void apply_values_();
|
||||
|
||||
MitsubishiCN105 hp_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); }
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
@@ -376,7 +376,7 @@ size_t ModbusController::create_register_ranges_() {
|
||||
while (ix != this->sensorset_.end()) {
|
||||
SensorItem *curr = *ix;
|
||||
|
||||
ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
|
||||
ESP_LOGV(TAG, "Register: 0x%X %d %d %zu offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
|
||||
curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
|
||||
|
||||
if (r.register_count == 0) {
|
||||
@@ -484,18 +484,18 @@ void ModbusController::dump_config() {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGCONFIG(TAG, "sensormap");
|
||||
for (auto &it : this->sensorset_) {
|
||||
ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d",
|
||||
ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu",
|
||||
static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
|
||||
it->get_register_size());
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "ranges");
|
||||
for (auto &it : this->register_ranges_) {
|
||||
ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
||||
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
||||
it.start_address, it.register_count, it.skip_updates);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "server registers");
|
||||
for (auto &r : this->server_registers_) {
|
||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address,
|
||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
|
||||
static_cast<uint8_t>(r->value_type), r->register_count);
|
||||
}
|
||||
#endif
|
||||
@@ -524,7 +524,7 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty
|
||||
void ModbusController::dump_sensors_() {
|
||||
ESP_LOGV(TAG, "sensors");
|
||||
for (auto &it : this->sensorset_) {
|
||||
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count,
|
||||
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count,
|
||||
it->get_register_size(), it->offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ enum OTAResponseTypes {
|
||||
OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A,
|
||||
OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B,
|
||||
OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C,
|
||||
OTA_RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D,
|
||||
OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "esphome/components/md5/md5.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_task_wdt.h>
|
||||
@@ -10,6 +11,8 @@
|
||||
|
||||
namespace esphome::ota {
|
||||
|
||||
static const char *const TAG = "ota.idf";
|
||||
|
||||
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
|
||||
|
||||
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
||||
@@ -98,7 +101,12 @@ OTAResponseTypes IDFOTABackend::end() {
|
||||
}
|
||||
}
|
||||
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||
#ifdef USE_OTA_SIGNED_VERIFICATION
|
||||
ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err);
|
||||
return OTA_RESPONSE_ERROR_SIGNATURE_INVALID;
|
||||
#else
|
||||
return OTA_RESPONSE_ERROR_UPDATE_END;
|
||||
#endif
|
||||
}
|
||||
if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
|
||||
return OTA_RESPONSE_ERROR_WRITING_FLASH;
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -29,6 +30,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA9554Component),
|
||||
cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -43,6 +45,8 @@ async def to_code(config):
|
||||
cg.add(var.set_pin_count(config[CONF_PIN_COUNT]))
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -34,12 +34,26 @@ void PCA9554Component::setup() {
|
||||
this->read_inputs_();
|
||||
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||
this->status_has_error());
|
||||
}
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read — only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCA9554Component::loop() {
|
||||
// Invalidate the cache at the start of each loop.
|
||||
// The actual read will happen on demand when digital_read() is called
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// Interrupt-driven: disable loop until next interrupt fires
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA9554Component::dump_config() {
|
||||
@@ -47,6 +61,7 @@ void PCA9554Component::dump_config() {
|
||||
"PCA9554:\n"
|
||||
" I/O Pins: %d",
|
||||
this->pin_count_);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -76,6 +91,11 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
// Clear mode mask bit
|
||||
this->config_mask_ &= ~(1 << pin);
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->config_mask_ |= 1 << pin;
|
||||
|
||||
@@ -16,7 +16,6 @@ class PCA9554Component : public Component,
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Invalidate cache at start of each loop
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
@@ -26,8 +25,11 @@ class PCA9554Component : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
|
||||
|
||||
bool read_inputs_();
|
||||
bool write_register_(uint8_t reg, uint16_t value);
|
||||
|
||||
@@ -48,6 +50,7 @@ class PCA9554Component : public Component,
|
||||
uint16_t input_mask_{0x00};
|
||||
/// Storage for last I2C error seen
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Helper class to expose a PCA9554 pin as an internal input GPIO pin.
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCF8574Component),
|
||||
cv.Optional(CONF_PCF8575, default=False): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -39,6 +41,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_pcf8575(config[CONF_PCF8575]))
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -15,16 +15,33 @@ void PCF8574Component::setup() {
|
||||
|
||||
this->write_gpio_();
|
||||
this->read_gpio_();
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read — only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCF8574Component::loop() {
|
||||
// Invalidate the cache at the start of each loop
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// Interrupt-driven: disable loop until next interrupt fires
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
void PCF8574Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"PCF8574:\n"
|
||||
" Is PCF8575: %s",
|
||||
YESNO(this->pcf8575_));
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -51,6 +68,11 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->mode_mask_ &= ~(1 << pin);
|
||||
// Write GPIO to enable input mode
|
||||
this->write_gpio_();
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->mode_mask_ |= 1 << pin;
|
||||
|
||||
@@ -17,10 +17,10 @@ class PCF8574Component : public Component,
|
||||
PCF8574Component() = default;
|
||||
|
||||
void set_pcf8575(bool pcf8575) { pcf8575_ = pcf8575; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Invalidate cache at start of each loop
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
@@ -30,6 +30,8 @@ class PCF8574Component : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCF8574Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -44,6 +46,7 @@ class PCF8574Component : public Component,
|
||||
/// The state read in read_gpio_ - 1 means HIGH, 0 means LOW
|
||||
uint16_t input_mask_{0x00};
|
||||
bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Helper class to expose a PCF8574 pin as an internal input GPIO pin.
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -33,6 +34,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component),
|
||||
cv.Optional(CONF_RESET, default=True): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -46,6 +48,8 @@ async def to_code(config):
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_reset(config[CONF_RESET]))
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -33,9 +33,24 @@ void PI4IOE5V6408Component::setup() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No need to clear latched interrupts before attaching the ISR — if INT is
|
||||
// already low the ISR fires immediately, loop runs, cache invalidates, and
|
||||
// the read clears the latch. One harmless extra read at most.
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PI4IOE5V6408Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PI4IOE5V6408:");
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -55,12 +70,22 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->pull_up_down_mask_ &= ~(1 << pin);
|
||||
this->pull_enable_mask_ |= 1 << pin;
|
||||
}
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
}
|
||||
// Write GPIO to enable input mode
|
||||
this->write_gpio_modes_();
|
||||
}
|
||||
|
||||
void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); }
|
||||
void PI4IOE5V6408Component::loop() {
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
bool PI4IOE5V6408Component::read_gpio_outputs_() {
|
||||
if (this->is_failed())
|
||||
@@ -142,6 +167,13 @@ bool PI4IOE5V6408Component::write_gpio_modes_() {
|
||||
this->status_set_warning(LOG_STR("Failed to write GPIO pull enable"));
|
||||
return false;
|
||||
}
|
||||
// Enable interrupts for input pins when interrupt pin is configured
|
||||
// (input pins have mode_mask_ bit cleared)
|
||||
if (this->interrupt_pin_ != nullptr &&
|
||||
!this->write_byte(PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK, static_cast<uint8_t>(~this->mode_mask_))) {
|
||||
this->status_set_warning(LOG_STR("Failed to write interrupt enable mask"));
|
||||
return false;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG,
|
||||
"Wrote GPIO config:\n"
|
||||
|
||||
@@ -22,8 +22,11 @@ class PI4IOE5V6408Component : public Component,
|
||||
|
||||
/// Indicate if the component should reset the state during setup
|
||||
void set_reset(bool reset) { this->reset_ = reset; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -40,6 +43,7 @@ class PI4IOE5V6408Component : public Component,
|
||||
uint8_t pull_up_down_mask_{0x00};
|
||||
|
||||
bool reset_{true};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
|
||||
bool read_gpio_modes_();
|
||||
bool write_gpio_modes_();
|
||||
|
||||
@@ -248,6 +248,9 @@ void RuntimeImage::release_buffer_() {
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
#ifdef USE_LVGL
|
||||
memset(&this->dsc_, 0, sizeof(this->dsc_));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ void SelectCall::perform() {
|
||||
|
||||
auto idx = target_index.value();
|
||||
// All operations use indices, call control() by index to avoid string conversion
|
||||
ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx));
|
||||
ESP_LOGV(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx));
|
||||
parent->control(idx);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ void SPIComponent::dump_config() {
|
||||
LOG_PIN(" SDI Pin: ", this->sdi_pin_);
|
||||
LOG_PIN(" SDO Pin: ", this->sdo_pin_);
|
||||
for (size_t i = 0; i != this->data_pins_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]);
|
||||
ESP_LOGCONFIG(TAG, " Data pin %zu: GPIO%d", i, this->data_pins_[i]);
|
||||
}
|
||||
if (this->spi_bus_->is_hw()) {
|
||||
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_);
|
||||
@@ -118,4 +118,12 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) {
|
||||
return out_data;
|
||||
}
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_ARDUINO)
|
||||
// Stub for unsupported platforms (host, Zephyr, etc.) - hardware SPI is unavailable
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::spi
|
||||
|
||||
@@ -23,9 +23,9 @@ using SPIInterface = SPIClassRP2040 *;
|
||||
using SPIInterface = SPIClass *;
|
||||
#endif
|
||||
|
||||
#elif defined(CLANG_TIDY)
|
||||
#elif defined(USE_HOST) || defined(CLANG_TIDY)
|
||||
|
||||
using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr)
|
||||
using SPIInterface = void *; // Stub for platforms without SPI (e.g., host, Zephyr)
|
||||
|
||||
#endif // USE_ESP32 / USE_ARDUINO
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
st7735_ns = cg.esphome_ns.namespace("st7735")
|
||||
|
||||
DEPRECATED_COMPONENT = """
|
||||
The 'st7735' component is deprecated and no new models will be added to it.
|
||||
New model PRs should target the newer and more performant 'mipi_spi' component.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
@@ -15,6 +17,7 @@ from esphome.const import (
|
||||
from . import st7735_ns
|
||||
|
||||
CODEOWNERS = ["@SenexCrenshaw"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
@@ -87,6 +90,9 @@ async def setup_st7735(var, config):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
LOGGER.warning(
|
||||
"The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||
)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_MODEL],
|
||||
|
||||
@@ -18,15 +18,15 @@ void Switch::control(bool target_state) {
|
||||
}
|
||||
}
|
||||
void Switch::turn_on() {
|
||||
ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str());
|
||||
ESP_LOGV(TAG, "'%s' Turning ON.", this->get_name().c_str());
|
||||
this->write_state(!this->inverted_);
|
||||
}
|
||||
void Switch::turn_off() {
|
||||
ESP_LOGD(TAG, "'%s' Turning OFF.", this->get_name().c_str());
|
||||
ESP_LOGV(TAG, "'%s' Turning OFF.", this->get_name().c_str());
|
||||
this->write_state(this->inverted_);
|
||||
}
|
||||
void Switch::toggle() {
|
||||
ESP_LOGD(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON");
|
||||
ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON");
|
||||
this->write_state(this->inverted_ == this->state);
|
||||
}
|
||||
optional<bool> Switch::get_initial_state() {
|
||||
|
||||
@@ -123,8 +123,8 @@ def _parse_cron_part(part, min_value, max_value, special_mapping):
|
||||
f"Can't have more than two '/' in one time expression, got {part}"
|
||||
)
|
||||
offset, repeat = data
|
||||
offset_n = 0
|
||||
if offset:
|
||||
offset_n = min_value
|
||||
if offset and offset not in ("*", "?"):
|
||||
offset_n = _parse_cron_int(
|
||||
offset,
|
||||
special_mapping,
|
||||
|
||||
@@ -20,7 +20,12 @@ bool CronTrigger::matches(const ESPTime &time) {
|
||||
return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] &&
|
||||
this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week];
|
||||
}
|
||||
void CronTrigger::loop() {
|
||||
void CronTrigger::setup() {
|
||||
// Cron resolution is 1 second — check once per second instead of every loop iteration
|
||||
this->set_interval(1000, [this]() { this->check_time_(); });
|
||||
}
|
||||
|
||||
void CronTrigger::check_time_() {
|
||||
ESPTime time = this->rtc_->now();
|
||||
if (!time.is_valid())
|
||||
return;
|
||||
|
||||
@@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component {
|
||||
void add_day_of_week(uint8_t day_of_week);
|
||||
void add_days_of_week(const std::vector<uint8_t> &days_of_week);
|
||||
bool matches(const ESPTime &time);
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void check_time_();
|
||||
std::bitset<61> seconds_;
|
||||
std::bitset<60> minutes_;
|
||||
std::bitset<24> hours_;
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
#include "total_daily_energy.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace total_daily_energy {
|
||||
namespace esphome::total_daily_energy {
|
||||
|
||||
static const char *const TAG = "total_daily_energy";
|
||||
static constexpr uint32_t TIMEOUT_ID_MIDNIGHT = 1;
|
||||
static constexpr uint8_t SECONDS_PER_MINUTE = 60;
|
||||
static constexpr uint8_t MINUTES_PER_HOUR = 60;
|
||||
static constexpr uint8_t HOURS_PER_DAY = 24;
|
||||
static constexpr uint32_t SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
|
||||
static constexpr uint16_t MILLIS_PER_SECOND = 1000;
|
||||
// Wake up 90 minutes before midnight to recalculate, ensuring DST transitions
|
||||
// (which shift wall clock by 1 hour but don't change millis()) don't cause
|
||||
// the midnight reset to fire late. DST transitions don't trigger the time sync
|
||||
// callback since they change local time interpretation, not the epoch.
|
||||
static constexpr uint32_t PRE_MIDNIGHT_SECONDS = 90 * SECONDS_PER_MINUTE;
|
||||
|
||||
void TotalDailyEnergy::setup() {
|
||||
float initial_value = 0;
|
||||
@@ -15,28 +26,55 @@ void TotalDailyEnergy::setup() {
|
||||
}
|
||||
this->publish_state_and_save(initial_value);
|
||||
|
||||
this->last_update_ = millis();
|
||||
this->last_update_ = App.get_loop_component_start_time();
|
||||
|
||||
this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); });
|
||||
|
||||
// Schedule initial midnight reset if time is already valid, otherwise
|
||||
// the time sync callback will handle it once time becomes available.
|
||||
this->schedule_midnight_reset_();
|
||||
// Re-schedule on every NTP sync in case the clock jumped across midnight.
|
||||
this->time_->add_on_time_sync_callback([this]() { this->schedule_midnight_reset_(); });
|
||||
}
|
||||
|
||||
void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); }
|
||||
|
||||
void TotalDailyEnergy::loop() {
|
||||
void TotalDailyEnergy::schedule_midnight_reset_() {
|
||||
auto t = this->time_->now();
|
||||
if (!t.is_valid())
|
||||
return;
|
||||
|
||||
if (this->last_day_of_year_ == 0) {
|
||||
// Check if the day changed (time sync moved us past midnight, or first call)
|
||||
if (this->last_day_of_year_ != t.day_of_year) {
|
||||
if (this->last_day_of_year_ != 0) {
|
||||
// Day actually changed — reset energy
|
||||
this->total_energy_ = 0;
|
||||
this->publish_state_and_save(0);
|
||||
}
|
||||
this->last_day_of_year_ = t.day_of_year;
|
||||
return;
|
||||
}
|
||||
|
||||
if (t.day_of_year != this->last_day_of_year_) {
|
||||
this->last_day_of_year_ = t.day_of_year;
|
||||
this->total_energy_ = 0;
|
||||
this->publish_state_and_save(0);
|
||||
// Calculate seconds until next midnight.
|
||||
// Uses the same TIMEOUT_ID_MIDNIGHT ID so re-scheduling (e.g. from time sync) cancels
|
||||
// any previously pending timeout.
|
||||
uint32_t seconds_until_midnight =
|
||||
((HOURS_PER_DAY - 1 - t.hour) * MINUTES_PER_HOUR + (MINUTES_PER_HOUR - 1 - t.minute)) * SECONDS_PER_MINUTE +
|
||||
(SECONDS_PER_MINUTE - t.second);
|
||||
|
||||
// set_timeout counts real elapsed millis, but DST shifts wall clock by up to 1 hour
|
||||
// without changing millis. To avoid firing up to 1 hour late/early, we use two stages:
|
||||
// 1) Wake up 90 minutes before midnight to recalculate with current wall clock
|
||||
// 2) From there, schedule the precise midnight reset
|
||||
uint32_t timeout_seconds;
|
||||
if (seconds_until_midnight > PRE_MIDNIGHT_SECONDS) {
|
||||
timeout_seconds = seconds_until_midnight - PRE_MIDNIGHT_SECONDS;
|
||||
} else {
|
||||
timeout_seconds = seconds_until_midnight + 1;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds);
|
||||
this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND,
|
||||
[this]() { this->schedule_midnight_reset_(); });
|
||||
}
|
||||
|
||||
void TotalDailyEnergy::publish_state_and_save(float state) {
|
||||
@@ -50,14 +88,14 @@ void TotalDailyEnergy::publish_state_and_save(float state) {
|
||||
void TotalDailyEnergy::process_new_state_(float state) {
|
||||
if (std::isnan(state))
|
||||
return;
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const float old_state = this->last_power_state_;
|
||||
const float new_state = state;
|
||||
float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f;
|
||||
float delta_hours = (now - this->last_update_) / static_cast<float>(MILLIS_PER_SECOND) / SECONDS_PER_HOUR;
|
||||
float delta_energy = 0.0f;
|
||||
switch (this->method_) {
|
||||
case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID:
|
||||
delta_energy = delta_hours * (old_state + new_state) / 2.0;
|
||||
delta_energy = delta_hours * (old_state + new_state) / 2.0f;
|
||||
break;
|
||||
case TOTAL_DAILY_ENERGY_METHOD_LEFT:
|
||||
delta_energy = delta_hours * old_state;
|
||||
@@ -71,5 +109,4 @@ void TotalDailyEnergy::process_new_state_(float state) {
|
||||
this->publish_state_and_save(this->total_energy_ + delta_energy);
|
||||
}
|
||||
|
||||
} // namespace total_daily_energy
|
||||
} // namespace esphome
|
||||
} // namespace esphome::total_daily_energy
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace total_daily_energy {
|
||||
namespace esphome::total_daily_energy {
|
||||
|
||||
enum TotalDailyEnergyMethod {
|
||||
TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0,
|
||||
@@ -23,12 +22,12 @@ class TotalDailyEnergy : public sensor::Sensor, public Component {
|
||||
void set_method(TotalDailyEnergyMethod method) { method_ = method; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void publish_state_and_save(float state);
|
||||
|
||||
protected:
|
||||
void process_new_state_(float state);
|
||||
void schedule_midnight_reset_();
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
time::RealTimeClock *time_;
|
||||
@@ -41,5 +40,4 @@ class TotalDailyEnergy : public sensor::Sensor, public Component {
|
||||
float last_power_state_{0.0f};
|
||||
};
|
||||
|
||||
} // namespace total_daily_energy
|
||||
} // namespace esphome
|
||||
} // namespace esphome::total_daily_energy
|
||||
|
||||
@@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char
|
||||
this->send(message, event, id, reconnect);
|
||||
}
|
||||
|
||||
void DeferredUpdateEventSourceList::loop() {
|
||||
bool DeferredUpdateEventSourceList::loop() {
|
||||
for (DeferredUpdateEventSource *dues : *this) {
|
||||
dues->loop();
|
||||
}
|
||||
return !this->empty();
|
||||
}
|
||||
|
||||
void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type,
|
||||
@@ -318,6 +319,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer
|
||||
es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); });
|
||||
|
||||
es->handleRequest(request);
|
||||
ws->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) {
|
||||
@@ -413,13 +415,24 @@ void WebServer::setup() {
|
||||
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
|
||||
// getting a lot of events
|
||||
this->set_interval(10000, [this]() {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
char buf[32];
|
||||
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
||||
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
|
||||
});
|
||||
}
|
||||
void WebServer::loop() { this->events_.loop(); }
|
||||
void WebServer::loop() {
|
||||
// No SSE clients connected; stop looping until a new client connects via
|
||||
// enable_loop_soon_any_context(). This is safe because:
|
||||
// - set_interval/set_timeout/defer run via the Scheduler, independent of loop()
|
||||
// - deferrable_send_state early-outs when no clients are connected
|
||||
// - try_send_nodefer (log, ping) iterates sessions which are empty
|
||||
// - REST API handlers use defer() which runs via the Scheduler
|
||||
if (!this->events_.loop())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
|
||||
@@ -169,7 +169,8 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent
|
||||
void on_client_disconnect_(DeferredUpdateEventSource *source);
|
||||
|
||||
public:
|
||||
void loop();
|
||||
/// Returns true if there are event sources remaining (including pending cleanup).
|
||||
bool loop();
|
||||
|
||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
|
||||
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
|
||||
|
||||
@@ -484,9 +484,12 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
this->on_connect_(rsp);
|
||||
}
|
||||
this->sessions_.push_back(rsp);
|
||||
// Wake up WebServer::loop() to drain deferred event queues for this client.
|
||||
// Safe from httpd task context via the pending_enable_loop_ flag.
|
||||
this->web_server_->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
void AsyncEventSource::loop() {
|
||||
bool AsyncEventSource::loop() {
|
||||
// Clean up dead sessions safely
|
||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||
// and the main loop handles the actual cleanup to avoid race conditions
|
||||
@@ -504,6 +507,7 @@ void AsyncEventSource::loop() {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return !this->sessions_.empty();
|
||||
}
|
||||
|
||||
void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
|
||||
|
||||
@@ -340,7 +340,8 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
|
||||
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
|
||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
|
||||
void loop();
|
||||
/// Returns true if there are sessions remaining (including pending cleanup).
|
||||
bool loop();
|
||||
bool empty() { return this->count() == 0; }
|
||||
|
||||
size_t count() const { return this->sessions_.size(); }
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
|
||||
#define USE_OTA_ROLLBACK
|
||||
#define USE_OTA_SIGNED_VERIFICATION
|
||||
#define USE_ESP32_MIN_CHIP_REVISION_SET
|
||||
#define USE_ESP32_SRAM1_AS_IRAM
|
||||
|
||||
@@ -330,6 +331,7 @@
|
||||
// ESP8266-specific feature flags
|
||||
#ifdef USE_ESP8266
|
||||
#define USE_ADC_SENSOR_VCC
|
||||
#define USE_ESP8266_CRASH_HANDLER
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2)
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_ESP8266_LOGGER_SERIAL
|
||||
|
||||
@@ -40,6 +40,8 @@ RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88
|
||||
RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89
|
||||
RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A
|
||||
RESPONSE_ERROR_MD5_MISMATCH = 0x8B
|
||||
RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C
|
||||
RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D
|
||||
RESPONSE_ERROR_UNKNOWN = 0xFF
|
||||
|
||||
OTA_VERSION_1_0 = 1
|
||||
@@ -192,6 +194,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None
|
||||
"Error: Application MD5 code mismatch. Please try again "
|
||||
"or flash over USB with a good quality cable."
|
||||
)
|
||||
if dat == RESPONSE_ERROR_SIGNATURE_INVALID:
|
||||
raise OTAError(
|
||||
"Error: Firmware signature verification failed. The firmware was not signed "
|
||||
"with the correct key. Ensure the signing key matches the one used to build "
|
||||
"the firmware currently running on the device."
|
||||
)
|
||||
if dat == RESPONSE_ERROR_UNKNOWN:
|
||||
raise OTAError("Unknown error from ESP")
|
||||
if not isinstance(expect, (list, tuple)):
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.2
|
||||
esphome-dashboard==20260210.0
|
||||
aioesphomeapi==44.8.1
|
||||
aioesphomeapi==44.9.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
+1
-3
@@ -15,8 +15,6 @@ from typing import Any
|
||||
|
||||
import colorama
|
||||
|
||||
from esphome.loader import get_platform
|
||||
|
||||
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", "..")))
|
||||
basepath = os.path.join(root_path, "esphome")
|
||||
temp_folder = os.path.join(root_path, ".temp")
|
||||
@@ -644,7 +642,7 @@ def get_all_dependencies(
|
||||
PLATFORM_HOST,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.loader import get_component
|
||||
from esphome.loader import get_component, get_platform
|
||||
|
||||
all_components: set[str] = set(component_names)
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
#include "esphome/components/light/color_mode.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- ListEntitiesSensorResponse ---
|
||||
|
||||
static ListEntitiesSensorResponse make_sensor_response() {
|
||||
ListEntitiesSensorResponse msg;
|
||||
msg.object_id = StringRef::from_lit("living_room_temperature");
|
||||
msg.key = 0x12345678;
|
||||
msg.name = StringRef::from_lit("Living Room Temperature");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = StringRef::from_lit("mdi:thermometer");
|
||||
#endif
|
||||
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
|
||||
msg.disabled_by_default = false;
|
||||
msg.unit_of_measurement = StringRef::from_lit("°C");
|
||||
msg.accuracy_decimals = 1;
|
||||
msg.force_update = false;
|
||||
msg.device_class = StringRef::from_lit("temperature");
|
||||
msg.state_class = enums::STATE_CLASS_MEASUREMENT;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = 1;
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_ListEntitiesSensorResponse);
|
||||
|
||||
static void Encode_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_ListEntitiesSensorResponse);
|
||||
|
||||
static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_ListEntitiesSensorResponse);
|
||||
|
||||
// --- ListEntitiesBinarySensorResponse ---
|
||||
|
||||
static ListEntitiesBinarySensorResponse make_binary_sensor_response() {
|
||||
ListEntitiesBinarySensorResponse msg;
|
||||
msg.object_id = StringRef::from_lit("front_door_contact");
|
||||
msg.key = 0xAABBCCDD;
|
||||
msg.name = StringRef::from_lit("Front Door Contact");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = StringRef::from_lit("mdi:door");
|
||||
#endif
|
||||
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
|
||||
msg.disabled_by_default = false;
|
||||
msg.device_class = StringRef::from_lit("door");
|
||||
msg.is_status_binary_sensor = false;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = 2;
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_ListEntitiesBinarySensorResponse);
|
||||
|
||||
static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_ListEntitiesBinarySensorResponse);
|
||||
|
||||
static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse);
|
||||
|
||||
// --- ListEntitiesLightResponse ---
|
||||
|
||||
static light::ColorModeMask light_color_modes;
|
||||
static FixedVector<const char *> light_effects;
|
||||
|
||||
static ListEntitiesLightResponse make_light_response() {
|
||||
// Initialize static data on first call
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
light_color_modes.insert(light::ColorMode::RGB_WHITE);
|
||||
light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE);
|
||||
light_effects.init(3);
|
||||
light_effects.push_back("None");
|
||||
light_effects.push_back("Rainbow");
|
||||
light_effects.push_back("Strobe");
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
ListEntitiesLightResponse msg;
|
||||
msg.object_id = StringRef::from_lit("kitchen_ceiling_light");
|
||||
msg.key = 0x55667788;
|
||||
msg.name = StringRef::from_lit("Kitchen Ceiling Light");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = StringRef::from_lit("mdi:ceiling-light");
|
||||
#endif
|
||||
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
|
||||
msg.disabled_by_default = false;
|
||||
msg.supported_color_modes = &light_color_modes;
|
||||
msg.min_mireds = 153.0f;
|
||||
msg.max_mireds = 500.0f;
|
||||
msg.effects = &light_effects;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = 3;
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_ListEntitiesLightResponse);
|
||||
|
||||
static void Encode_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_ListEntitiesLightResponse);
|
||||
|
||||
static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_ListEntitiesLightResponse);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -133,6 +133,6 @@ def test_code_generation(
|
||||
assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp
|
||||
assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp
|
||||
assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp
|
||||
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
|
||||
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for mpip_spi configuration validation."""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -12,6 +13,16 @@ from esphome.core import CORE
|
||||
from esphome.pins import gpio_pin_schema
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_spi_final_validate():
|
||||
"""Mock spi.final_validate_device_schema since unit tests have no real SPI bus config."""
|
||||
with mock.patch(
|
||||
"esphome.components.spi.final_validate_device_schema",
|
||||
return_value=lambda config: None,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
|
||||
"""
|
||||
|
||||
@@ -25,7 +25,9 @@ from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
def validated_config(config):
|
||||
"""Run schema + final validation and return the validated config."""
|
||||
return FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
config = CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
def test_metadata_native_quad_default_test_card(
|
||||
|
||||
@@ -4,3 +4,14 @@ sensor:
|
||||
tvoc:
|
||||
name: AGS10 TVOC
|
||||
update_interval: 60s
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test AGS10 Actions"
|
||||
on_press:
|
||||
- ags10.set_zero_point:
|
||||
id: ags10_1
|
||||
mode: CURRENT_VALUE
|
||||
- ags10.new_i2c_address:
|
||||
id: ags10_1
|
||||
address: 0x1A
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
*** DO NOT USE THIS KEY...EVER ***
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIG5AIBAAKCAYEA0J665DlxzUzzouzH96fxqXybEfFU7H1oSf2fUHwoNMgUG7Vc
|
||||
SHxuFJkpsUnxg9br09/v5THOXfUj5t/Arog6FGiL7i0HXCYDMnSn2EzQWR+DY2Qj
|
||||
C3YzTLvcOQ40gFjDWfzAheAMCQmc5xQeB3YmaXQf+fUWH/PfFs9Pm+L92YTv2XC1
|
||||
B2q5s8K8hUWghO472A+UMrreuDcltNJ+TbuSRHK0NQzKpKo0Vkl4HycczGDgpa8D
|
||||
h68JL/BKVeJAjKxWd/xcj/FCk661ODXi0esB/mGQP3hAthWpwi+gdkWczWs1Ocr6
|
||||
VxKje1zFm9SEq+SmCViPY/Pu8Xs7steqz3b3JtRGtKQE0r3B+hBKI7aRudOZyz0s
|
||||
kqoL1zYrAWmoTWBqa2tj1ACPqtr2LyHGt2aVrHRQGJf21mPYIy9GIOv+3v3GzIAK
|
||||
az2B8Z93Bw1biwNZDr1SLNYfQVaJT1hQavmdlvwW8vqLUGDcQlk42yOF6nAmvAPu
|
||||
Wzxf+QFEtJT6Am65AgMBAAECggGAB0d+mG+LscDtYGI4MQNGaqZLJ+NelfjjPm+v
|
||||
0yhd48eWcggQPgQ/eA8HFiVRHMtPQ7+U2I+2Fm+zDr+AcuaUdjlWppsiHlxCMMzC
|
||||
vYiinXV8yWdJVMFNVXBZpRECknbmbBmmYxV3/gm8lJCOYq7D9NqFMhzT5o4FGv/l
|
||||
VHhlaKVblB/7ZRSbgbL6DoFpMjI42tdiUanVEyLzeR1+JDq3BhXlhVNar8ezl04t
|
||||
d5LPDa+UrxtN+XpJTQeqpFgGbhImSxjzCjo0kbGiEx/DwWuFJxguIcDU25sM4g2+
|
||||
ivtn7N11U0oaqNwsz7p4cKAm8toJYxxXWZvKdj1kZvCZ+BtyH0/MtOa2Q6v91HOh
|
||||
zY4KEl5wxQYnxJrgqevSm8rrC51tLOCidZ16cHba8sjrK69xysEazk43roHLFXDp
|
||||
JpH7Zd8LETjFWGVfUz6vppzkt6mrJk0DNuMLk/UwpPHzW2pu1qDiHPCb9+ra1S9U
|
||||
t55hT2TBFDcG/NmZZnyHQoh8METhAoHBAPIG0G8Cd4fmkvbgCRLzCIWsH6zGHS9o
|
||||
80Rj9Gu93B+m/F9GtgyYuX+DKSdMdw3IJamUsBwofT2wynmkuJFhLtD1FmYtlsXf
|
||||
TWp8g8CfFGrIXDvin5E3heyhvtFiOjXlw0Q8yMmQXr5LF0i3WyFCPQM20ugClB7N
|
||||
CQBOVAfpVoRU1fA6UjjHibFRwi1b4bLV69QiERPCJfcny/DPkZpu7I1fiINmwzEb
|
||||
O5mIFo5F4TQADEreWplXEmhEXIzDMFIwEQKBwQDcqimCcO3RSysZMQhhUfk8G19I
|
||||
yRNwvi2fK5LiGCZMYjeYKqg1rBN4yCf9PTwaqBNRqXTg13Fc7zrOkSI+0oDa4FWI
|
||||
/kMEztaUK+Kwd2NKc96aXHMBGF+1Sx7Ygnr9e2dyqDqRij2/qlQYY1EDz7cYldaX
|
||||
YNrXcQQeNJbqydjRDYi+9bI+wDkrK/5PxE1sGmqS1RMKxoJCZmxNiQT3PmXM/oNR
|
||||
Ev6N9CDklFtClWNcD0Uum+mxNJ53ldZDx4UI/CkCgcEA6R6BI3vX0FHaGureMp9f
|
||||
BQoulEdbEzBeqPAyHJkKbn50Nf0xGt78RYL7X7v6LI8tH7N1Eho5z/L6g8KSeI2H
|
||||
/4MiqRaeVEdrFPeMHDvd+aC1noUBt2komS2OU7XuZb3CoHZ/3A4wA9DmQ4dAwr8/
|
||||
b1oeOZVKQISzd9T6gYhSajIgwzwZuFESInaitvf6ZDxC49hQZJyr3u05NeFo2Lyh
|
||||
Iuby4cZYmnMlrBN1zmImseSd8ntL/sjslPvLvVXAtFlRAoHBAIDuG5rPiOTE2sW5
|
||||
VIAoeUuZYq8QbX9uXxGlUAkyuw3eRUVvhyD1DduAd30Ljla05bTNIjFNMDtwvBd9
|
||||
zViPfiJk+RU2GspwYAfrLGSXHTifQu1GHxwAtcsjvT4b3ujEdckUakQnVbTrPH+T
|
||||
Z/6mGwEOa3e/a559tj4/0/4TOc/L7J5GyILJpZ2H8uuAcww60xI/1QRywCEz3wve
|
||||
hzw/BRQlkWyJgJpIjf+Af2IEDy327iExj/WuHPkaXzrzFNQPIQKBwAM6qeNOxrO3
|
||||
V91wg4+44FAsOda62fZ0GlCM7ETnEjLbFamtCKEcDNfijwTa54LcZ6yObyutD1RN
|
||||
dhj4Z6QKuYnsE02agv9CtXdFEVEXaqj4pshdgVOwGK34OidT4yIJQGLrRAQ/JiGH
|
||||
x6CoGUCNIAq5J08VdosLTD9qdn1zv8USCAP0ReKnRMndTzENLYz9G3nQyHgt5GzI
|
||||
YoSRtrWnXrQp2Yn3epk74gFAJtKozWNV4Du35FJBjmSeMuRivonNMQ==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
*** DO NOT USE THIS KEY...EVER ***
|
||||
@@ -0,0 +1,10 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
signing_key: ../../components/esp32/dummy_signing_key.pem
|
||||
signing_scheme: rsa3072
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1 +1,20 @@
|
||||
<<: !include common-w5500.yaml
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 19
|
||||
mosi_pin: 21
|
||||
miso_pin: 23
|
||||
cs_pin: 18
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
subnet: 255.255.255.0
|
||||
domain: .local
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
interface: spi2
|
||||
on_connect:
|
||||
- logger.log: "Ethernet connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Ethernet disconnected!"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -49,6 +49,7 @@ lvgl:
|
||||
bg_color: 0x000000
|
||||
bg_opa: cover
|
||||
theme:
|
||||
dark_mode: true
|
||||
obj:
|
||||
border_width: 1
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
mcp23008:
|
||||
i2c_id: i2c_bus
|
||||
id: mcp23008_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23008_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23008_hub_int
|
||||
address: 0x21
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -9,6 +13,12 @@ binary_sensor:
|
||||
mcp23xxx: mcp23008_hub
|
||||
number: 0
|
||||
mode: INPUT
|
||||
- platform: gpio
|
||||
id: mcp23008_binary_sensor_int
|
||||
pin:
|
||||
mcp23xxx: mcp23008_hub_int
|
||||
number: 0
|
||||
mode: INPUT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
mcp23017:
|
||||
i2c_id: i2c_bus
|
||||
id: mcp23017_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23017_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23017_hub_int
|
||||
address: 0x21
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -9,6 +13,12 @@ binary_sensor:
|
||||
mcp23xxx: mcp23017_hub
|
||||
number: 0
|
||||
mode: INPUT
|
||||
- platform: gpio
|
||||
id: mcp23017_binary_sensor_int
|
||||
pin:
|
||||
mcp23xxx: mcp23017_hub_int
|
||||
number: 0
|
||||
mode: INPUT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ mcp23s08:
|
||||
- id: mcp23s08_hub
|
||||
cs_pin: ${cs_pin}
|
||||
deviceaddress: 0
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO15
|
||||
interrupt_pin: GPIO0
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
|
||||
@@ -2,3 +2,4 @@ mcp23s17:
|
||||
- id: mcp23s17_hub
|
||||
cs_pin: ${cs_pin}
|
||||
deviceaddress: 0
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user