Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
d24ba3727a [esp32_ble] Refactor ESPBTUUID::from_raw to use parse_hex helpers 2025-10-06 12:12:28 -05:00
56 changed files with 249 additions and 999 deletions

View File

@@ -1 +1 @@
ab49c22900dd39c004623e450a1076b111d6741f31967a637ab6e0e3dd2e753e 499db61c1aa55b98b6629df603a56a1ba7aff5a9a7c781a5c1552a9dcd186c08

View File

@@ -6,7 +6,6 @@ on:
- ".clang-tidy" - ".clang-tidy"
- "platformio.ini" - "platformio.ini"
- "requirements_dev.txt" - "requirements_dev.txt"
- "sdkconfig.defaults"
- ".clang-tidy.hash" - ".clang-tidy.hash"
- "script/clang_tidy_hash.py" - "script/clang_tidy_hash.py"
- ".github/workflows/ci-clang-tidy-hash.yml" - ".github/workflows/ci-clang-tidy-hash.yml"

View File

@@ -9,7 +9,6 @@ import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTION, CONF_ACTION,
CONF_ACTIONS, CONF_ACTIONS,
CONF_CAPTURE_RESPONSE,
CONF_DATA, CONF_DATA,
CONF_DATA_TEMPLATE, CONF_DATA_TEMPLATE,
CONF_EVENT, CONF_EVENT,
@@ -18,50 +17,30 @@ from esphome.const import (
CONF_MAX_CONNECTIONS, CONF_MAX_CONNECTIONS,
CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED, CONF_ON_CLIENT_DISCONNECTED,
CONF_ON_ERROR,
CONF_ON_SUCCESS,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PORT, CONF_PORT,
CONF_REBOOT_TIMEOUT, CONF_REBOOT_TIMEOUT,
CONF_RESPONSE_TEMPLATE,
CONF_SERVICE, CONF_SERVICE,
CONF_SERVICES, CONF_SERVICES,
CONF_TAG, CONF_TAG,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VARIABLES, CONF_VARIABLES,
) )
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigType from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "api" DOMAIN = "api"
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used."""
base = ["socket"]
# Check if any homeassistant.action/homeassistant.service has capture_response: true
# This flag is set during config validation in _validate_response_config
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
return base + ["json"]
return base
api_ns = cg.esphome_ns.namespace("api") api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
HomeAssistantServiceCallAction = api_ns.class_( HomeAssistantServiceCallAction = api_ns.class_(
"HomeAssistantServiceCallAction", automation.Action "HomeAssistantServiceCallAction", automation.Action
) )
ActionResponse = api_ns.class_("ActionResponse")
HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger
)
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
@@ -309,29 +288,6 @@ async def to_code(config):
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
def _validate_response_config(config: ConfigType) -> ConfigType:
# Validate dependencies:
# - response_template requires capture_response: true
# - capture_response: true requires on_success
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
raise cv.Invalid(
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
path=[CONF_RESPONSE_TEMPLATE],
)
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
raise cv.Invalid(
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
path=[CONF_CAPTURE_RESPONSE],
)
# Track if any action uses capture_response for AUTO_LOAD
if config[CONF_CAPTURE_RESPONSE]:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
return config
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@@ -347,15 +303,10 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Optional(CONF_VARIABLES, default={}): cv.Schema( cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda} {cv.string: cv.returning_lambda}
), ),
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
} }
), ),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION),
_validate_response_config,
) )
@@ -369,12 +320,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
HomeAssistantServiceCallAction, HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA, HOMEASSISTANT_ACTION_ACTION_SCHEMA,
) )
async def homeassistant_service_to_code( async def homeassistant_service_to_code(config, action_id, template_arg, args):
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False) var = cg.new_Pvariable(action_id, template_arg, serv, False)
@@ -389,40 +335,6 @@ async def homeassistant_service_to_code(
for key, value in config[CONF_VARIABLES].items(): for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ)) cg.add(var.add_variable(key, templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
cg.add(var.set_wants_status())
await automation.build_automation(
var.get_error_trigger(),
[(cg.std_string, "error"), *args],
on_error,
)
if on_success := config.get(CONF_ON_SUCCESS):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add(var.set_wants_status())
if config[CONF_CAPTURE_RESPONSE]:
cg.add(var.set_wants_response())
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
await automation.build_automation(
var.get_success_trigger_with_response(),
[(cg.JsonObjectConst, "response"), *args],
on_success,
)
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
templ = await cg.templatable(response_template, args, cg.std_string)
cg.add(var.set_response_template(templ))
else:
await automation.build_automation(
var.get_success_trigger(),
args,
on_success,
)
return var return var

View File

@@ -780,22 +780,6 @@ message HomeassistantActionRequest {
repeated HomeassistantServiceMap data_template = 3; repeated HomeassistantServiceMap data_template = 3;
repeated HomeassistantServiceMap variables = 4; repeated HomeassistantServiceMap variables = 4;
bool is_event = 5; bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
}
// Message sent by Home Assistant to ESPHome with service call response data
message HomeassistantActionResponse {
option (id) = 130;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
} }
// ==================== IMPORT HOME ASSISTANT STATES ==================== // ==================== IMPORT HOME ASSISTANT STATES ====================

View File

@@ -8,9 +8,9 @@
#endif #endif
#include <cerrno> #include <cerrno>
#include <cinttypes> #include <cinttypes>
#include <utility>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <utility>
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
@@ -1549,20 +1549,6 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
} }
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (msg.response_data_len > 0) {
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
msg.response_data_len);
} else
#endif
{
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
}
};
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
NoiseEncryptionSetKeyResponse resp; NoiseEncryptionSetKeyResponse resp;

View File

@@ -129,10 +129,7 @@ class APIConnection final : public APIServerConnection {
return; return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;

View File

@@ -132,22 +132,24 @@ APIError APINoiseFrameHelper::loop() {
return APIFrameHelper::loop(); return APIFrameHelper::loop();
} }
/** Read a packet into the rx_buf_. /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
* *
* On success, rx_buf_ contains the frame data and state variables are cleared for the next read. * @param frame: The struct to hold the frame information in.
* Caller is responsible for consuming rx_buf_ (e.g., via std::move). * msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
* *
* @return APIError::OK if a full packet is in rx_buf_ * @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
* *
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet. * errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/ */
APIError APINoiseFrameHelper::try_read_frame_() { APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
// Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK) if (frame == nullptr) {
if (this->rx_buf_len_ == 0) { HELPER_LOG("Bad argument for try_read_frame_");
this->rx_buf_.clear(); return APIError::BAD_ARG;
} }
// read header // read header
@@ -190,9 +192,9 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
// Reserve space for body // reserve space for body
if (this->rx_buf_.size() != msg_size) { if (rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size); rx_buf_.resize(msg_size);
} }
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
@@ -210,12 +212,12 @@ APIError APINoiseFrameHelper::try_read_frame_() {
} }
} }
LOG_PACKET_RECEIVED(this->rx_buf_); LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// Clear state for next frame (rx_buf_ still contains data for caller) // consume msg
this->rx_buf_len_ = 0; rx_buf_ = {};
this->rx_header_buf_len_ = 0; rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK; return APIError::OK;
} }
@@ -237,17 +239,18 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::CLIENT_HELLO) { if (state_ == State::CLIENT_HELLO) {
// waiting for client hello // waiting for client hello
aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
// ignore contents, may be used in future for flags // ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data // Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size(); size_t old_size = prologue_.size();
this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); prologue_.resize(old_size + 2 + frame.size());
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); prologue_[old_size] = (uint8_t) (frame.size() >> 8);
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); prologue_[old_size + 1] = (uint8_t) frame.size();
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
state_ = State::SERVER_HELLO; state_ = State::SERVER_HELLO;
} }
@@ -289,23 +292,24 @@ APIError APINoiseFrameHelper::state_action_() {
int action = noise_handshakestate_get_action(handshake_); int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) { if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg // waiting for handshake msg
aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
if (this->rx_buf_.empty()) { if (frame.empty()) {
send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) { } else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); HELPER_LOG("Bad handshake error byte: %u", frame[0]);
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} }
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) { if (err != 0) {
// Special handling for MAC failure // Special handling for MAC failure
@@ -382,33 +386,35 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->state_action_(); int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return aerr; return aerr;
} }
if (this->state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
APIError decrypt_err = APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK) { if (decrypt_err != APIError::OK)
return decrypt_err; return decrypt_err;
}
uint16_t msg_size = mbuf.size; uint16_t msg_size = mbuf.size;
uint8_t *msg_data = this->rx_buf_.data(); uint8_t *msg_data = frame.data();
if (msg_size < 4) { if (msg_size < 4) {
this->state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size); HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
@@ -416,12 +422,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) { if (data_len > msg_size - 4) {
this->state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
buffer->container = std::move(this->rx_buf_); buffer->container = std::move(frame);
buffer->data_offset = 4; buffer->data_offset = 4;
buffer->data_len = data_len; buffer->data_len = data_len;
buffer->type = type; buffer->type = type;

View File

@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
protected: protected:
APIError state_action_(); APIError state_action_();
APIError try_read_frame_(); APIError try_read_frame_(std::vector<uint8_t> *frame);
APIError write_frame_(const uint8_t *data, uint16_t len); APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_(); APIError init_handshake_();
APIError check_handshake_finished_(); APIError check_handshake_finished_();

View File

@@ -47,19 +47,19 @@ APIError APIPlaintextFrameHelper::loop() {
return APIFrameHelper::loop(); return APIFrameHelper::loop();
} }
/** Read a packet into the rx_buf_. /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
* *
* On success, rx_buf_ contains the frame data and state variables are cleared for the next read. * @param frame: The struct to hold the frame information in.
* Caller is responsible for consuming rx_buf_ (e.g., via std::move). * msg: store the parsed frame in that struct
* *
* @return See APIError * @return See APIError
* *
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/ */
APIError APIPlaintextFrameHelper::try_read_frame_() { APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
// Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK) if (frame == nullptr) {
if (this->rx_buf_len_ == 0) { HELPER_LOG("Bad argument for try_read_frame_");
this->rx_buf_.clear(); return APIError::BAD_ARG;
} }
// read header // read header
@@ -150,9 +150,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
} }
// header reading done // header reading done
// Reserve space for body // reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) { if (rx_buf_.size() != rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_); rx_buf_.resize(rx_header_parsed_len_);
} }
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
@@ -170,22 +170,24 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
} }
} }
LOG_PACKET_RECEIVED(this->rx_buf_); LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// Clear state for next frame (rx_buf_ still contains data for caller) // consume msg
this->rx_buf_len_ = 0; rx_buf_ = {};
this->rx_header_buf_pos_ = 0; rx_buf_len_ = 0;
this->rx_header_parsed_ = false; rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) { APIError aerr;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
APIError aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) { if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't // Make sure to tell the remote that we don't
@@ -218,10 +220,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr; return aerr;
} }
buffer->container = std::move(this->rx_buf_); buffer->container = std::move(frame);
buffer->data_offset = 0; buffer->data_offset = 0;
buffer->data_len = this->rx_header_parsed_len_; buffer->data_len = rx_header_parsed_len_;
buffer->type = this->rx_header_parsed_type_; buffer->type = rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {

View File

@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected: protected:
APIError try_read_frame_(); APIError try_read_frame_(std::vector<uint8_t> *frame);
// Group 2-byte aligned types // Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0; uint16_t rx_header_parsed_type_ = 0;

View File

@@ -884,15 +884,6 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_message(4, it, true); buffer.encode_message(4, it, true);
} }
buffer.encode_bool(5, this->is_event); buffer.encode_bool(5, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
buffer.encode_uint32(6, this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_bool(7, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_string(8, this->response_template);
#endif
} }
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->service_ref_.size()); size.add_length(1, this->service_ref_.size());
@@ -900,48 +891,6 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->data_template);
size.add_repeated_message(1, this->variables); size.add_repeated_message(1, this->variables);
size.add_bool(1, this->is_event); size.add_bool(1, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
size.add_uint32(1, this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
size.add_bool(1, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
size.add_length(1, this->response_template.size());
#endif
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->call_id = value.as_uint32();
break;
case 2:
this->success = value.as_bool();
break;
default:
return false;
}
return true;
}
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 3:
this->error_message = value.as_string();
break;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
case 4: {
// Use raw data directly to avoid allocation
this->response_data = value.data();
this->response_data_len = value.size();
break;
}
#endif
default:
return false;
}
return true;
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
class HomeassistantActionRequest final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 128; static constexpr uint8_t ESTIMATED_SIZE = 113;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_request"; } const char *message_name() const override { return "homeassistant_action_request"; }
#endif #endif
@@ -1114,15 +1114,6 @@ class HomeassistantActionRequest final : public ProtoMessage {
std::vector<HomeassistantServiceMap> data_template{}; std::vector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{}; std::vector<HomeassistantServiceMap> variables{};
bool is_event{false}; bool is_event{false};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
uint32_t call_id{0};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
bool wants_response{false};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
std::string response_template{};
#endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1132,30 +1123,6 @@ class HomeassistantActionRequest final : public ProtoMessage {
protected: protected:
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
class HomeassistantActionResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 130;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_response"; }
#endif
uint32_t call_id{0};
bool success{false};
std::string error_message{};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
public: public:

View File

@@ -1122,28 +1122,6 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
out.append("\n"); out.append("\n");
} }
dump_field(out, "is_event", this->is_event); dump_field(out, "is_event", this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
dump_field(out, "call_id", this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_field(out, "wants_response", this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_field(out, "response_template", this->response_template);
#endif
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void HomeassistantActionResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantActionResponse");
dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
#endif
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -610,17 +610,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_z_wave_proxy_request(msg); this->on_z_wave_proxy_request(msg);
break; break;
} }
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
case HomeassistantActionResponse::MESSAGE_TYPE: {
HomeassistantActionResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
#endif
this->on_homeassistant_action_response(msg);
break;
}
#endif #endif
default: default:
break; break;

View File

@@ -66,9 +66,6 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
#endif #endif

View File

@@ -9,16 +9,12 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#include "esphome/core/version.h" #include "esphome/core/version.h"
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "homeassistant_service.h"
#endif
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
#endif #endif
#include <algorithm> #include <algorithm>
#include <utility>
namespace esphome::api { namespace esphome::api {
@@ -404,38 +400,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
client->send_homeassistant_action(call); client->send_homeassistant_action(call);
} }
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
}
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) {
auto callback = std::move(it->callback);
this->action_response_callbacks_.erase(it);
ActionResponse response(success, error_message);
callback(response);
return;
}
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) {
auto callback = std::move(it->callback);
this->action_response_callbacks_.erase(it);
ActionResponse response(success, error_message, response_data, response_data_len);
callback(response);
return;
}
}
}
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,

View File

@@ -16,7 +16,6 @@
#include "user_services.h" #include "user_services.h"
#endif #endif
#include <map>
#include <vector> #include <vector>
namespace esphome::api { namespace esphome::api {
@@ -112,17 +111,7 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif
// Action response handling
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif #endif
@@ -198,13 +187,6 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_; std::vector<UserServiceDescriptor *> user_services_;
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse {
uint32_t call_id;
ActionResponseCallback callback;
};
std::vector<PendingActionResponse> action_response_callbacks_;
#endif
// Group smaller types together // Group smaller types together
uint16_t port_{6053}; uint16_t port_{6053};

View File

@@ -3,13 +3,8 @@
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
#include <functional>
#include <utility>
#include <vector> #include <vector>
#include "api_pb2.h" #include "api_pb2.h"
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#include "esphome/components/json/json_util.h"
#endif
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -49,47 +44,9 @@ template<typename... Ts> class TemplatableKeyValuePair {
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
}; };
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action
class ActionResponse {
public:
ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
}
#endif
bool is_success() const { return this->success_; }
const std::string &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object (const version returns read-only view)
JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
#endif
protected:
bool success_;
std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_;
#endif
};
// Callback type for action responses
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
#endif
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> { template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
public: public:
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) { explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
this->flags_.is_event = is_event;
}
template<typename T> void set_service(T service) { this->service_ = service; } template<typename T> void set_service(T service) { this->service_ = service; }
@@ -104,29 +61,11 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->variables_.emplace_back(std::move(key), value); this->variables_.emplace_back(std::move(key), value);
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
this->flags_.has_response_template = true;
}
void set_wants_status() { this->flags_.wants_status = true; }
void set_wants_response() { this->flags_.wants_response = true; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
}
#endif
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event; resp.is_event = this->is_event_;
for (auto &it : this->data_) { for (auto &it : this->data_) {
resp.data.emplace_back(); resp.data.emplace_back();
auto &kv = resp.data.back(); auto &kv = resp.data.back();
@@ -145,74 +84,18 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.set_key(StringRef(it.key)); kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...); kv.value = it.value.value(x...);
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->flags_.wants_status) {
// Generate a unique call ID for this service call
static uint32_t call_id_counter = 1;
uint32_t call_id = call_id_counter++;
resp.call_id = call_id;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
resp.wants_response = true;
// Set response template if provided
if (this->flags_.has_response_template) {
std::string response_template_value = this->response_template_.value(x...);
resp.response_template = response_template_value;
}
}
#endif
auto captured_args = std::make_tuple(x...);
this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
std::apply(
[this, &response](auto &&...args) {
if (response.is_success()) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
this->success_trigger_with_response_->trigger(response.get_json(), args...);
} else
#endif
{
this->success_trigger_->trigger(args...);
}
} else {
this->error_trigger_->trigger(response.get_error_message(), args...);
}
},
captured_args);
});
}
#endif
this->parent_->send_homeassistant_action(resp); this->parent_->send_homeassistant_action(resp);
} }
protected: protected:
APIServer *parent_; APIServer *parent_;
bool is_event_;
TemplatableStringValue<Ts...> service_{}; TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_; std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_; std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_; std::vector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct Flags {
uint8_t is_event : 1;
uint8_t wants_status : 1;
uint8_t wants_response : 1;
uint8_t has_response_template : 1;
uint8_t reserved : 5;
} flags_{0};
}; };
} // namespace esphome::api } // namespace esphome::api
#endif #endif
#endif #endif

View File

@@ -35,7 +35,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
msg.set_name(StringRef(this->name_)); msg.set_name(StringRef(this->name_));
msg.key = this->key_; msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
for (size_t i = 0; i < sizeof...(Ts); i++) { for (int i = 0; i < sizeof...(Ts); i++) {
msg.args.emplace_back(); msg.args.emplace_back();
auto &arg = msg.args.back(); auto &arg = msg.args.back();
arg.type = arg_types[i]; arg.type = arg_types[i];

View File

@@ -131,16 +131,28 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
if (this->uuid_.len == uuid.uuid_.len) { if (this->uuid_.len == uuid.uuid_.len) {
switch (this->uuid_.len) { switch (this->uuid_.len) {
case ESP_UUID_LEN_16: case ESP_UUID_LEN_16:
return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16; if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) {
return true;
}
break;
case ESP_UUID_LEN_32: case ESP_UUID_LEN_32:
return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32; if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) {
return true;
}
break;
case ESP_UUID_LEN_128: case ESP_UUID_LEN_128:
return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0; for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) {
default: if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
return false; return false;
}
}
return true;
break;
} }
} else {
return this->as_128bit() == uuid.as_128bit();
} }
return this->as_128bit() == uuid.as_128bit(); return false;
} }
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const { std::string ESPBTUUID::to_string() const {

View File

@@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
if (b) { if (b) {
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
if (y >= y_offset && static_cast<uint32_t>(y) < y_offset + this->height_) if (y >= y_offset && y < y_offset + this->height_)
buff->draw_pixel_at(x, y, c); buff->draw_pixel_at(x, y, c);
}; };
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {

View File

@@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
this->real_control_packet_size_); this->real_control_packet_size_);
this->status_message_callback_.call((const char *) data, data_size); this->status_message_callback_.call((const char *) data, data_size);
} else { } else {
ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_); ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_);
} }
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
@@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
size_t expected_size = size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
if (size < expected_size) { if (size < expected_size) {
ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size); ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
} }
uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];

View File

@@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase {
int extra_control_packet_bytes_{0}; int extra_control_packet_bytes_{0};
int extra_sensors_packet_bytes_{4}; int extra_sensors_packet_bytes_{4};
int status_message_header_size_{0}; int status_message_header_size_{0};
size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4}; int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4};
HonControlMethod control_method_; HonControlMethod control_method_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_; std::queue<haier_protocol::HaierMessage> control_messages_queue_;

View File

@@ -5,7 +5,6 @@ from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CAPTURE_RESPONSE,
CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ESP8266_DISABLE_SSL_SUPPORT,
CONF_ID, CONF_ID,
CONF_METHOD, CONF_METHOD,
@@ -58,6 +57,7 @@ CONF_HEADERS = "headers"
CONF_COLLECT_HEADERS = "collect_headers" CONF_COLLECT_HEADERS = "collect_headers"
CONF_BODY = "body" CONF_BODY = "body"
CONF_JSON = "json" CONF_JSON = "json"
CONF_CAPTURE_RESPONSE = "capture_response"
def validate_url(value): def validate_url(value):

View File

@@ -20,23 +20,6 @@ bool MCP2515::setup_internal() {
return false; return false;
if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK)
return false; return false;
// setup hardware filter RXF0 accepting all standard CAN IDs
if (this->set_filter_(RXF::RXF0, false, 0) != canbus::ERROR_OK) {
return false;
}
if (this->set_filter_mask_(MASK::MASK0, false, 0) != canbus::ERROR_OK) {
return false;
}
// setup hardware filter RXF1 accepting all extended CAN IDs
if (this->set_filter_(RXF::RXF1, true, 0) != canbus::ERROR_OK) {
return false;
}
if (this->set_filter_mask_(MASK::MASK1, true, 0) != canbus::ERROR_OK) {
return false;
}
if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK)
return false; return false;
uint8_t err_flags = this->get_error_flags_(); uint8_t err_flags = this->get_error_flags_();

View File

@@ -26,16 +26,24 @@ void MDNSComponent::setup() {
mdns_instance_name_set(this->hostname_.c_str()); mdns_instance_name_set(this->hostname_.c_str());
for (const auto &service : this->services_) { for (const auto &service : this->services_) {
std::vector<mdns_txt_item_t> txt_records(service.txt_records.size()); std::vector<mdns_txt_item_t> txt_records;
for (size_t i = 0; i < service.txt_records.size(); i++) { for (const auto &record : service.txt_records) {
// mdns_service_add copies the strings internally, no need to strdup mdns_txt_item_t it{};
txt_records[i].key = service.txt_records[i].key.c_str(); // dup strings to ensure the pointer is valid even after the record loop
txt_records[i].value = const_cast<TemplatableValue<std::string> &>(service.txt_records[i].value).value().c_str(); it.key = strdup(record.key.c_str());
it.value = strdup(const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
txt_records.push_back(it);
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, service.service_type.c_str(), service.proto.c_str(), port, txt_records.data(), err = mdns_service_add(nullptr, service.service_type.c_str(), service.proto.c_str(), port, txt_records.data(),
txt_records.size()); txt_records.size());
// free records
for (const auto &it : txt_records) {
delete it.key; // NOLINT(cppcoreguidelines-owning-memory)
delete it.value; // NOLINT(cppcoreguidelines-owning-memory)
}
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to register service %s: %s", service.service_type.c_str(), esp_err_to_name(err)); ESP_LOGW(TAG, "Failed to register service %s: %s", service.service_type.c_str(), esp_err_to_name(err));
} }

View File

@@ -340,7 +340,7 @@ class MipiSpi : public display::Display,
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
} }
} else { } else {
for (size_t y = 0; y != static_cast<size_t>(h); y++) { for (size_t y = 0; y != h; y++) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w); this->write_array(ptr, w);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
@@ -372,8 +372,8 @@ class MipiSpi : public display::Display,
uint8_t dbuffer[DISPLAYPIXEL * 48]; uint8_t dbuffer[DISPLAYPIXEL * 48];
uint8_t *dptr = dbuffer; uint8_t *dptr = dbuffer;
auto stride = x_offset + w + x_pad; // stride in pixels auto stride = x_offset + w + x_pad; // stride in pixels
for (size_t y = 0; y != static_cast<size_t>(h); y++) { for (size_t y = 0; y != h; y++) {
for (size_t x = 0; x != static_cast<size_t>(w); x++) { for (size_t x = 0; x != w; x++) {
auto color_val = ptr[y * stride + x]; auto color_val = ptr[y * stride + x];
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
// 16 to 18 bit conversion // 16 to 18 bit conversion

View File

@@ -5,8 +5,6 @@ from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
CONF_CARRIER_DUTY_PERCENT,
CONF_CARRIER_FREQUENCY,
CONF_CLOCK_RESOLUTION, CONF_CLOCK_RESOLUTION,
CONF_DUMP, CONF_DUMP,
CONF_FILTER, CONF_FILTER,
@@ -151,14 +149,6 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
), ),
cv.boolean, cv.boolean,
), ),
cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All(
cv.only_on_esp32,
cv.percentage_int,
cv.Range(min=1, max=100),
),
cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All(
cv.only_on_esp32, cv.frequency, cv.int_
),
} }
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
@@ -178,8 +168,6 @@ async def to_code(config):
cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION]))
if CONF_FILTER_SYMBOLS in config: if CONF_FILTER_SYMBOLS in config:
cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS]))
cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT]))
cg.add(var.set_carrier_frequency(config[CONF_CARRIER_FREQUENCY]))
else: else:
var = cg.new_Pvariable(config[CONF_ID], pin) var = cg.new_Pvariable(config[CONF_ID], pin)

View File

@@ -64,8 +64,6 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; }
void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; }
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; }
void set_carrier_frequency(uint32_t carrier_frequency) { this->carrier_frequency_ = carrier_frequency; }
#endif #endif
void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; }
void set_filter_us(uint32_t filter_us) { this->filter_us_ = filter_us; } void set_filter_us(uint32_t filter_us) { this->filter_us_ = filter_us; }
@@ -78,8 +76,6 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
uint32_t filter_symbols_{0}; uint32_t filter_symbols_{0};
uint32_t receive_symbols_{0}; uint32_t receive_symbols_{0};
bool with_dma_{false}; bool with_dma_{false};
uint32_t carrier_frequency_{0};
uint8_t carrier_duty_percent_{100};
esp_err_t error_code_{ESP_OK}; esp_err_t error_code_{ESP_OK};
std::string error_string_{""}; std::string error_string_{""};
#endif #endif

View File

@@ -72,21 +72,6 @@ void RemoteReceiverComponent::setup() {
return; return;
} }
if (this->carrier_frequency_ > 0 && 0 < this->carrier_duty_percent_ && this->carrier_duty_percent_ < 100) {
rmt_carrier_config_t carrier;
memset(&carrier, 0, sizeof(carrier));
carrier.frequency_hz = this->carrier_frequency_;
carrier.duty_cycle = (float) this->carrier_duty_percent_ / 100.0f;
carrier.flags.polarity_active_low = this->pin_->is_inverted();
error = rmt_apply_carrier(this->channel_, &carrier);
if (error != ESP_OK) {
this->error_code_ = error;
this->error_string_ = "in rmt_apply_carrier";
this->mark_failed();
return;
}
}
rmt_rx_event_callbacks_t callbacks; rmt_rx_event_callbacks_t callbacks;
memset(&callbacks, 0, sizeof(callbacks)); memset(&callbacks, 0, sizeof(callbacks));
callbacks.on_recv_done = rmt_callback; callbacks.on_recv_done = rmt_callback;
@@ -126,13 +111,11 @@ void RemoteReceiverComponent::dump_config() {
" Filter symbols: %" PRIu32 "\n" " Filter symbols: %" PRIu32 "\n"
" Receive symbols: %" PRIu32 "\n" " Receive symbols: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n" " Tolerance: %" PRIu32 "%s\n"
" Carrier frequency: %" PRIu32 " hz\n"
" Carrier duty: %u%%\n"
" Filter out pulses shorter than: %" PRIu32 " us\n" " Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes", " Signal is done after %" PRIu32 " us of no changes",
this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_,
this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%",
this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); this->filter_us_, this->idle_us_);
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_),
this->error_string_.c_str()); this->error_string_.c_str());

View File

@@ -215,7 +215,7 @@ void Rtttl::loop() {
sample[x].right = 0; sample[x].right = 0;
} }
if (static_cast<size_t>(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
break; break;
} }
this->samples_sent_++; this->samples_sent_++;

View File

@@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() {
// Read a GateStatus from the unit. The unit only sends messages in response to // Read a GateStatus from the unit. The unit only sends messages in response to
// status requests or commands, so a message needs to be sent first. // status requests or commands, so a message needs to be sent first.
optional<GateStatus> Tormatic::read_gate_status_() { optional<GateStatus> Tormatic::read_gate_status_() {
if (this->available() < static_cast<int>(sizeof(MessageHeader))) { if (this->available() < sizeof(MessageHeader)) {
return {}; return {};
} }

View File

@@ -58,7 +58,7 @@ void UponorSmatrixClimate::control(const climate::ClimateCall &call) {
} }
void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) { void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) {
for (size_t i = 0; i < data_len; i++) { for (int i = 0; i < data_len; i++) {
switch (data[i].id) { switch (data[i].id) {
case UPONOR_ID_TARGET_TEMP_MIN: case UPONOR_ID_TARGET_TEMP_MIN:
this->min_temperature_ = raw_to_celsius(data[i].value); this->min_temperature_ = raw_to_celsius(data[i].value);

View File

@@ -18,7 +18,7 @@ void UponorSmatrixSensor::dump_config() {
} }
void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) { void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) {
for (size_t i = 0; i < data_len; i++) { for (int i = 0; i < data_len; i++) {
switch (data[i].id) { switch (data[i].id) {
case UPONOR_ID_ROOM_TEMP: case UPONOR_ID_ROOM_TEMP:
if (this->temperature_sensor_ != nullptr) if (this->temperature_sensor_ != nullptr)

View File

@@ -122,7 +122,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
// Decode packet payload data for easy access // Decode packet payload data for easy access
UponorSmatrixData data[data_len]; UponorSmatrixData data[data_len];
for (size_t i = 0; i < data_len; i++) { for (int i = 0; i < data_len; i++) {
data[i].id = packet[(i * 3) + 4]; data[i].id = packet[(i * 3) + 4];
data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]); data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]);
} }
@@ -135,7 +135,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
// thermostat sending both room temperature and time information. // thermostat sending both room temperature and time information.
bool found_temperature = false; bool found_temperature = false;
bool found_time = false; bool found_time = false;
for (size_t i = 0; i < data_len; i++) { for (int i = 0; i < data_len; i++) {
if (data[i].id == UPONOR_ID_ROOM_TEMP) if (data[i].id == UPONOR_ID_ROOM_TEMP)
found_temperature = true; found_temperature = true;
if (data[i].id == UPONOR_ID_DATETIME1) if (data[i].id == UPONOR_ID_DATETIME1)
@@ -181,7 +181,7 @@ bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixDa
packet.push_back(device_address >> 8); packet.push_back(device_address >> 8);
packet.push_back(device_address >> 0); packet.push_back(device_address >> 0);
for (size_t i = 0; i < data_len; i++) { for (int i = 0; i < data_len; i++) {
packet.push_back(data[i].id); packet.push_back(data[i].id);
packet.push_back(data[i].value >> 8); packet.push_back(data[i].value >> 8);
packet.push_back(data[i].value >> 0); packet.push_back(data[i].value >> 0);

View File

@@ -1,7 +1,6 @@
#include "veml7700.h" #include "veml7700.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <limits>
namespace esphome { namespace esphome {
namespace veml7700 { namespace veml7700 {
@@ -13,30 +12,30 @@ static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; }
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) { template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
size_t i = 0; size_t i = 0;
size_t idx = std::numeric_limits<size_t>::max(); size_t idx = -1;
while (idx == std::numeric_limits<size_t>::max() && i < size) { while (idx == -1 && i < size) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i++; i++;
} }
if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size) if (idx == -1 || i + 1 >= size)
return val; return val;
return array[i + 1]; return array[i + 1];
} }
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1; size_t i = size - 1;
size_t idx = std::numeric_limits<size_t>::max(); size_t idx = -1;
while (idx == std::numeric_limits<size_t>::max() && i > 0) { while (idx == -1 && i > 0) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i--; i--;
} }
if (idx == std::numeric_limits<size_t>::max() || i == 0) if (idx == -1 || i == 0)
return val; return val;
return array[i - 1]; return array[i - 1];
} }

View File

@@ -2274,11 +2274,11 @@ void GDEW0154M09::clear_() {
uint32_t pixsize = this->get_buffer_length_(); uint32_t pixsize = this->get_buffer_length_();
for (uint8_t j = 0; j < 2; j++) { for (uint8_t j = 0; j < 2; j++) {
this->command(CMD_DTM1_DATA_START_TRANS); this->command(CMD_DTM1_DATA_START_TRANS);
for (uint32_t count = 0; count < pixsize; count++) { for (int count = 0; count < pixsize; count++) {
this->data(0x00); this->data(0x00);
} }
this->command(CMD_DTM2_DATA_START_TRANS2); this->command(CMD_DTM2_DATA_START_TRANS2);
for (uint32_t count = 0; count < pixsize; count++) { for (int count = 0; count < pixsize; count++) {
this->data(0xff); this->data(0xff);
} }
this->command(CMD_DISPLAY_REFRESH); this->command(CMD_DISPLAY_REFRESH);
@@ -2291,11 +2291,11 @@ void HOT GDEW0154M09::display() {
this->init_internal_(); this->init_internal_();
// "Mode 0 display" for now // "Mode 0 display" for now
this->command(CMD_DTM1_DATA_START_TRANS); this->command(CMD_DTM1_DATA_START_TRANS);
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { for (int i = 0; i < this->get_buffer_length_(); i++) {
this->data(0xff); this->data(0xff);
} }
this->command(CMD_DTM2_DATA_START_TRANS2); // write 'new' data to SRAM this->command(CMD_DTM2_DATA_START_TRANS2); // write 'new' data to SRAM
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { for (int i = 0; i < this->get_buffer_length_(); i++) {
this->data(this->buffer_[i]); this->data(this->buffer_[i]);
} }
this->command(CMD_DISPLAY_REFRESH); this->command(CMD_DISPLAY_REFRESH);

View File

@@ -67,31 +67,6 @@ ConfigPath = list[str | int]
path_context = contextvars.ContextVar("Config path") path_context = contextvars.ContextVar("Config path")
def _add_auto_load_steps(result: Config, loads: list[str]) -> None:
"""Add AutoLoadValidationStep for each component in loads that isn't already loaded."""
for load in loads:
if load not in result:
result.add_validation_step(AutoLoadValidationStep(load))
def _process_auto_load(
result: Config, platform: ComponentManifest, path: ConfigPath
) -> None:
# Process platform's AUTO_LOAD
auto_load = platform.auto_load
if isinstance(auto_load, list):
_add_auto_load_steps(result, auto_load)
elif callable(auto_load):
import inspect
if inspect.signature(auto_load).parameters:
result.add_validation_step(
AddDynamicAutoLoadsValidationStep(path, platform)
)
else:
_add_auto_load_steps(result, auto_load())
def _process_platform_config( def _process_platform_config(
result: Config, result: Config,
component_name: str, component_name: str,
@@ -116,7 +91,9 @@ def _process_platform_config(
CORE.loaded_platforms.add(f"{component_name}/{platform_name}") CORE.loaded_platforms.add(f"{component_name}/{platform_name}")
# Process platform's AUTO_LOAD # Process platform's AUTO_LOAD
_process_auto_load(result, platform, path) for load in platform.auto_load:
if load not in result:
result.add_validation_step(AutoLoadValidationStep(load))
# Add validation steps for the platform # Add validation steps for the platform
p_domain = f"{component_name}.{platform_name}" p_domain = f"{component_name}.{platform_name}"
@@ -413,7 +390,9 @@ class LoadValidationStep(ConfigValidationStep):
result[self.domain] = self.conf = [self.conf] result[self.domain] = self.conf = [self.conf]
# Process AUTO_LOAD # Process AUTO_LOAD
_process_auto_load(result, component, path) for load in component.auto_load:
if load not in result:
result.add_validation_step(AutoLoadValidationStep(load))
result.add_validation_step( result.add_validation_step(
MetadataValidationStep([self.domain], self.domain, self.conf, component) MetadataValidationStep([self.domain], self.domain, self.conf, component)
@@ -639,34 +618,6 @@ class MetadataValidationStep(ConfigValidationStep):
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
class AddDynamicAutoLoadsValidationStep(ConfigValidationStep):
"""Add dynamic auto loads step.
This step is used to auto-load components where one component can alter its
AUTO_LOAD based on its configuration.
"""
# Has to happen after normal schema is validated and before final schema validation
priority = -10.0
def __init__(self, path: ConfigPath, comp: ComponentManifest) -> None:
self.path = path
self.comp = comp
def run(self, result: Config) -> None:
if result.errors:
# If result already has errors, skip this step
return
conf = result.get_nested_item(self.path)
with result.catch_error(self.path):
auto_load = self.comp.auto_load
if not callable(auto_load):
return
loads = auto_load(conf)
_add_auto_load_steps(result, loads)
class SchemaValidationStep(ConfigValidationStep): class SchemaValidationStep(ConfigValidationStep):
"""Schema validation step. """Schema validation step.

View File

@@ -174,7 +174,6 @@ CONF_CALIBRATE_LINEAR = "calibrate_linear"
CONF_CALIBRATION = "calibration" CONF_CALIBRATION = "calibration"
CONF_CAPACITANCE = "capacitance" CONF_CAPACITANCE = "capacitance"
CONF_CAPACITY = "capacity" CONF_CAPACITY = "capacity"
CONF_CAPTURE_RESPONSE = "capture_response"
CONF_CARBON_MONOXIDE = "carbon_monoxide" CONF_CARBON_MONOXIDE = "carbon_monoxide"
CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent"
CONF_CARRIER_FREQUENCY = "carrier_frequency" CONF_CARRIER_FREQUENCY = "carrier_frequency"
@@ -677,7 +676,6 @@ CONF_ON_RESPONSE = "on_response"
CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SHUTDOWN = "on_shutdown"
CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_SPEED_SET = "on_speed_set"
CONF_ON_STATE = "on_state" CONF_ON_STATE = "on_state"
CONF_ON_SUCCESS = "on_success"
CONF_ON_TAG = "on_tag" CONF_ON_TAG = "on_tag"
CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TAG_REMOVED = "on_tag_removed"
CONF_ON_TIME = "on_time" CONF_ON_TIME = "on_time"
@@ -820,7 +818,6 @@ CONF_RESET_DURATION = "reset_duration"
CONF_RESET_PIN = "reset_pin" CONF_RESET_PIN = "reset_pin"
CONF_RESIZE = "resize" CONF_RESIZE = "resize"
CONF_RESOLUTION = "resolution" CONF_RESOLUTION = "resolution"
CONF_RESPONSE_TEMPLATE = "response_template"
CONF_RESTART = "restart" CONF_RESTART = "restart"
CONF_RESTORE = "restore" CONF_RESTORE = "restore"
CONF_RESTORE_MODE = "restore_mode" CONF_RESTORE_MODE = "restore_mode"

View File

@@ -112,8 +112,6 @@
#define USE_API #define USE_API
#define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_CONNECTED_TRIGGER
#define USE_API_CLIENT_DISCONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#define USE_API_HOMEASSISTANT_SERVICES #define USE_API_HOMEASSISTANT_SERVICES
#define USE_API_HOMEASSISTANT_STATES #define USE_API_HOMEASSISTANT_STATES
#define USE_API_NOISE #define USE_API_NOISE

View File

@@ -77,7 +77,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
&hour, // NOLINT &hour, // NOLINT
&minute, // NOLINT &minute, // NOLINT
&second, &num) == 6 && // NOLINT &second, &num) == 6 && // NOLINT
num == static_cast<int>(time_to_parse.size())) { num == time_to_parse.size()) {
esp_time.year = year; esp_time.year = year;
esp_time.month = month; esp_time.month = month;
esp_time.day_of_month = day; esp_time.day_of_month = day;
@@ -87,7 +87,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT &hour, // NOLINT
&minute, &num) == 5 && // NOLINT &minute, &num) == 5 && // NOLINT
num == static_cast<int>(time_to_parse.size())) { num == time_to_parse.size()) {
esp_time.year = year; esp_time.year = year;
esp_time.month = month; esp_time.month = month;
esp_time.day_of_month = day; esp_time.day_of_month = day;
@@ -95,17 +95,17 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
esp_time.minute = minute; esp_time.minute = minute;
esp_time.second = 0; esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) { num == time_to_parse.size()) {
esp_time.hour = hour; esp_time.hour = hour;
esp_time.minute = minute; esp_time.minute = minute;
esp_time.second = second; esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == static_cast<int>(time_to_parse.size())) { num == time_to_parse.size()) {
esp_time.hour = hour; esp_time.hour = hour;
esp_time.minute = minute; esp_time.minute = minute;
esp_time.second = 0; esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) { num == time_to_parse.size()) {
esp_time.year = year; esp_time.year = year;
esp_time.month = month; esp_time.month = month;
esp_time.day_of_month = day; esp_time.day_of_month = day;

View File

@@ -82,10 +82,11 @@ class ComponentManifest:
return getattr(self.module, "CONFLICTS_WITH", []) return getattr(self.module, "CONFLICTS_WITH", [])
@property @property
def auto_load( def auto_load(self) -> list[str]:
self, al = getattr(self.module, "AUTO_LOAD", [])
) -> list[str] | Callable[[], list[str]] | Callable[[ConfigType], list[str]]: if callable(al):
return getattr(self.module, "AUTO_LOAD", []) return al()
return al
@property @property
def codeowners(self) -> list[str]: def codeowners(self) -> list[str]:

View File

@@ -13,7 +13,7 @@ esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20250904.0 esphome-dashboard==20250904.0
aioesphomeapi==41.11.0 aioesphomeapi==41.11.0
zeroconf==0.148.0 zeroconf==0.147.2
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml==0.18.15 # dashboard_import
ruamel.yaml.clib==0.2.12 # dashboard_import ruamel.yaml.clib==0.2.12 # dashboard_import

View File

@@ -1,4 +1,4 @@
pylint==3.3.9 pylint==3.3.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.13.3 # also change in .pre-commit-config.yaml when updating ruff==0.13.3 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating

View File

@@ -48,10 +48,9 @@ def parse_requirement_line(line: str) -> tuple[str, str] | None:
return None return None
def get_clang_tidy_version_from_requirements(repo_root: Path | None = None) -> str: def get_clang_tidy_version_from_requirements() -> str:
"""Get clang-tidy version from requirements_dev.txt""" """Get clang-tidy version from requirements_dev.txt"""
repo_root = _ensure_repo_root(repo_root) requirements_path = Path(__file__).parent.parent / "requirements_dev.txt"
requirements_path = repo_root / "requirements_dev.txt"
lines = read_file_lines(requirements_path) lines = read_file_lines(requirements_path)
for line in lines: for line in lines:
@@ -69,49 +68,30 @@ def read_file_bytes(path: Path) -> bytes:
return f.read() return f.read()
def get_repo_root() -> Path: def calculate_clang_tidy_hash() -> str:
"""Get the repository root directory."""
return Path(__file__).parent.parent
def _ensure_repo_root(repo_root: Path | None) -> Path:
"""Ensure repo_root is a Path, using default if None."""
return repo_root if repo_root is not None else get_repo_root()
def calculate_clang_tidy_hash(repo_root: Path | None = None) -> str:
"""Calculate hash of clang-tidy configuration and version""" """Calculate hash of clang-tidy configuration and version"""
repo_root = _ensure_repo_root(repo_root)
hasher = hashlib.sha256() hasher = hashlib.sha256()
# Hash .clang-tidy file # Hash .clang-tidy file
clang_tidy_path = repo_root / ".clang-tidy" clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy"
content = read_file_bytes(clang_tidy_path) content = read_file_bytes(clang_tidy_path)
hasher.update(content) hasher.update(content)
# Hash clang-tidy version from requirements_dev.txt # Hash clang-tidy version from requirements_dev.txt
version = get_clang_tidy_version_from_requirements(repo_root) version = get_clang_tidy_version_from_requirements()
hasher.update(version.encode()) hasher.update(version.encode())
# Hash the entire platformio.ini file # Hash the entire platformio.ini file
platformio_path = repo_root / "platformio.ini" platformio_path = Path(__file__).parent.parent / "platformio.ini"
platformio_content = read_file_bytes(platformio_path) platformio_content = read_file_bytes(platformio_path)
hasher.update(platformio_content) hasher.update(platformio_content)
# Hash sdkconfig.defaults file
sdkconfig_path = repo_root / "sdkconfig.defaults"
if sdkconfig_path.exists():
sdkconfig_content = read_file_bytes(sdkconfig_path)
hasher.update(sdkconfig_content)
return hasher.hexdigest() return hasher.hexdigest()
def read_stored_hash(repo_root: Path | None = None) -> str | None: def read_stored_hash() -> str | None:
"""Read the stored hash from file""" """Read the stored hash from file"""
repo_root = _ensure_repo_root(repo_root) hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
hash_file = repo_root / ".clang-tidy.hash"
if hash_file.exists(): if hash_file.exists():
lines = read_file_lines(hash_file) lines = read_file_lines(hash_file)
return lines[0].strip() if lines else None return lines[0].strip() if lines else None
@@ -124,10 +104,9 @@ def write_file_content(path: Path, content: str) -> None:
f.write(content) f.write(content)
def write_hash(hash_value: str, repo_root: Path | None = None) -> None: def write_hash(hash_value: str) -> None:
"""Write hash to file""" """Write hash to file"""
repo_root = _ensure_repo_root(repo_root) hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
hash_file = repo_root / ".clang-tidy.hash"
# Strip any trailing newlines to ensure consistent formatting # Strip any trailing newlines to ensure consistent formatting
write_file_content(hash_file, hash_value.strip() + "\n") write_file_content(hash_file, hash_value.strip() + "\n")
@@ -155,28 +134,8 @@ def main() -> None:
stored_hash = read_stored_hash() stored_hash = read_stored_hash()
if args.check: if args.check:
# Check if hash changed OR if .clang-tidy.hash was updated in this PR # Exit 0 if full scan needed (hash changed or no hash file)
# This is used in CI to determine if a full clang-tidy scan is needed sys.exit(0 if current_hash != stored_hash else 1)
hash_changed = current_hash != stored_hash
# Lazy import to avoid requiring dependencies that aren't needed for other modes
from helpers import changed_files # noqa: E402
hash_file_updated = ".clang-tidy.hash" in changed_files()
# Exit 0 if full scan needed
sys.exit(0 if (hash_changed or hash_file_updated) else 1)
elif args.verify:
# Verify that hash file is up to date with current configuration
# This is used in pre-commit and CI checks to ensure hash was updated
if current_hash != stored_hash:
print("ERROR: Clang-tidy configuration has changed but hash not updated!")
print(f"Expected: {current_hash}")
print(f"Found: {stored_hash}")
print("\nPlease run: script/clang_tidy_hash.py --update")
sys.exit(1)
print("Hash verification passed")
elif args.update: elif args.update:
write_hash(current_hash) write_hash(current_hash)
@@ -192,6 +151,15 @@ def main() -> None:
print("Clang-tidy hash unchanged") print("Clang-tidy hash unchanged")
sys.exit(0) sys.exit(0)
elif args.verify:
if current_hash != stored_hash:
print("ERROR: Clang-tidy configuration has changed but hash not updated!")
print(f"Expected: {current_hash}")
print(f"Found: {stored_hash}")
print("\nPlease run: script/clang_tidy_hash.py --update")
sys.exit(1)
print("Hash verification passed")
else: else:
print(f"Current hash: {current_hash}") print(f"Current hash: {current_hash}")
print(f"Stored hash: {stored_hash}") print(f"Stored hash: {stored_hash}")

View File

@@ -529,16 +529,7 @@ def get_all_dependencies(component_names: set[str]) -> set[str]:
new_components.update(dep.split(".")[0] for dep in comp.dependencies) new_components.update(dep.split(".")[0] for dep in comp.dependencies)
# Add auto_load components # Add auto_load components
auto_load = comp.auto_load new_components.update(comp.auto_load)
if callable(auto_load):
import inspect
if inspect.signature(auto_load).parameters:
auto_load = auto_load(None)
else:
auto_load = auto_load()
new_components.update(auto_load)
# Check if we found any new components # Check if we found any new components
new_components -= all_components new_components -= all_components

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
from collections.abc import Callable
from pathlib import Path from pathlib import Path
import sys import sys
@@ -14,7 +13,7 @@ from esphome.const import (
PLATFORM_ESP8266, PLATFORM_ESP8266,
) )
from esphome.core import CORE from esphome.core import CORE
from esphome.loader import ComponentManifest, get_component, get_platform from esphome.loader import get_component, get_platform
def filter_component_files(str): def filter_component_files(str):
@@ -46,29 +45,6 @@ def add_item_to_components_graph(components_graph, parent, child):
components_graph[parent].append(child) components_graph[parent].append(child)
def resolve_auto_load(
auto_load: list[str] | Callable[[], list[str]] | Callable[[dict | None], list[str]],
config: dict | None = None,
) -> list[str]:
"""Resolve AUTO_LOAD to a list, handling callables with or without config parameter.
Args:
auto_load: The AUTO_LOAD value (list or callable)
config: Optional config to pass to callable AUTO_LOAD functions
Returns:
List of component names to auto-load
"""
if not callable(auto_load):
return auto_load
import inspect
if inspect.signature(auto_load).parameters:
return auto_load(config)
return auto_load()
def create_components_graph(): def create_components_graph():
# The root directory of the repo # The root directory of the repo
root = Path(__file__).parent.parent root = Path(__file__).parent.parent
@@ -87,7 +63,7 @@ def create_components_graph():
components_graph = {} components_graph = {}
platforms = [] platforms = []
components: list[tuple[ComponentManifest, str, Path]] = [] components = []
for path in components_dir.iterdir(): for path in components_dir.iterdir():
if not path.is_dir(): if not path.is_dir():
@@ -116,8 +92,8 @@ def create_components_graph():
for target_config in TARGET_CONFIGURATIONS: for target_config in TARGET_CONFIGURATIONS:
CORE.data[KEY_CORE] = target_config CORE.data[KEY_CORE] = target_config
for item in resolve_auto_load(comp.auto_load, config=None): for auto_load in comp.auto_load:
add_item_to_components_graph(components_graph, item, name) add_item_to_components_graph(components_graph, auto_load, name)
# restore config # restore config
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
@@ -138,8 +114,8 @@ def create_components_graph():
for target_config in TARGET_CONFIGURATIONS: for target_config in TARGET_CONFIGURATIONS:
CORE.data[KEY_CORE] = target_config CORE.data[KEY_CORE] = target_config
for item in resolve_auto_load(platform.auto_load, config={}): for auto_load in platform.auto_load:
add_item_to_components_graph(components_graph, item, name) add_item_to_components_graph(components_graph, auto_load, name)
# restore config # restore config
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]

View File

@@ -13,7 +13,6 @@ CONFIG_ESP_TASK_WDT=y
CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_PANIC=y
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
CONFIG_AUTOSTART_ARDUINO=y
# esp32_ble # esp32_ble
CONFIG_BT_ENABLED=y CONFIG_BT_ENABLED=y

View File

@@ -10,39 +10,6 @@ esphome:
data: data:
message: Button was pressed message: Button was pressed
- homeassistant.tag_scanned: pulse - homeassistant.tag_scanned: pulse
- homeassistant.action:
action: weather.get_forecasts
data:
entity_id: weather.forecast_home
type: hourly
capture_response: true
on_success:
- lambda: |-
JsonObject next_hour = response["response"]["weather.forecast_home"]["forecast"][0];
float next_temperature = next_hour["temperature"].as<float>();
ESP_LOGD("main", "Next hour temperature: %f", next_temperature);
on_error:
- lambda: |-
ESP_LOGE("main", "Action failed with error: %s", error.c_str());
- homeassistant.action:
action: weather.get_forecasts
data:
entity_id: weather.forecast_home
type: hourly
capture_response: true
response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}"
on_success:
- lambda: |-
float temperature = response["response"].as<float>();
ESP_LOGD("main", "Next hour temperature: %f", temperature);
- homeassistant.action:
action: light.toggle
data:
entity_id: light.demo_light
on_success:
- logger.log: "Toggled demo light"
on_error:
- logger.log: "Failed to toggle demo light"
api: api:
port: 8000 port: 8000

View File

@@ -1,8 +1,6 @@
substitutions: substitutions:
pin: GPIO2 pin: GPIO2
clock_resolution: "2000000" clock_resolution: "2000000"
carrier_duty_percent: "25"
carrier_frequency: "30000"
filter_symbols: "2" filter_symbols: "2"
receive_symbols: "4" receive_symbols: "4"
rmt_symbols: "64" rmt_symbols: "64"

View File

@@ -44,53 +44,37 @@ def test_get_clang_tidy_version_from_requirements(
assert result == expected assert result == expected
def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None: def test_calculate_clang_tidy_hash() -> None:
"""Test calculating hash from all configuration sources including sdkconfig.defaults.""" """Test calculating hash from all configuration sources."""
clang_tidy_content = b"Checks: '-*,readability-*'\n" clang_tidy_content = b"Checks: '-*,readability-*'\n"
requirements_version = "clang-tidy==18.1.5" requirements_version = "clang-tidy==18.1.5"
platformio_content = b"[env:esp32]\nplatform = espressif32\n" platformio_content = b"[env:esp32]\nplatform = espressif32\n"
sdkconfig_content = b"CONFIG_AUTOSTART_ARDUINO=y\n"
requirements_content = "clang-tidy==18.1.5\n"
# Create temporary files
(tmp_path / ".clang-tidy").write_bytes(clang_tidy_content)
(tmp_path / "platformio.ini").write_bytes(platformio_content)
(tmp_path / "sdkconfig.defaults").write_bytes(sdkconfig_content)
(tmp_path / "requirements_dev.txt").write_text(requirements_content)
# Expected hash calculation # Expected hash calculation
expected_hasher = hashlib.sha256() expected_hasher = hashlib.sha256()
expected_hasher.update(clang_tidy_content) expected_hasher.update(clang_tidy_content)
expected_hasher.update(requirements_version.encode()) expected_hasher.update(requirements_version.encode())
expected_hasher.update(platformio_content) expected_hasher.update(platformio_content)
expected_hasher.update(sdkconfig_content)
expected_hash = expected_hasher.hexdigest() expected_hash = expected_hasher.hexdigest()
result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path) # Mock the dependencies
with (
patch("clang_tidy_hash.read_file_bytes") as mock_read_bytes,
patch(
"clang_tidy_hash.get_clang_tidy_version_from_requirements",
return_value=requirements_version,
),
):
# Set up mock to return different content based on the file being read
def read_file_mock(path: Path) -> bytes:
if ".clang-tidy" in str(path):
return clang_tidy_content
if "platformio.ini" in str(path):
return platformio_content
return b""
assert result == expected_hash mock_read_bytes.side_effect = read_file_mock
result = clang_tidy_hash.calculate_clang_tidy_hash()
def test_calculate_clang_tidy_hash_without_sdkconfig(tmp_path: Path) -> None:
"""Test calculating hash without sdkconfig.defaults file."""
clang_tidy_content = b"Checks: '-*,readability-*'\n"
requirements_version = "clang-tidy==18.1.5"
platformio_content = b"[env:esp32]\nplatform = espressif32\n"
requirements_content = "clang-tidy==18.1.5\n"
# Create temporary files (without sdkconfig.defaults)
(tmp_path / ".clang-tidy").write_bytes(clang_tidy_content)
(tmp_path / "platformio.ini").write_bytes(platformio_content)
(tmp_path / "requirements_dev.txt").write_text(requirements_content)
# Expected hash calculation (no sdkconfig)
expected_hasher = hashlib.sha256()
expected_hasher.update(clang_tidy_content)
expected_hasher.update(requirements_version.encode())
expected_hasher.update(platformio_content)
expected_hash = expected_hasher.hexdigest()
result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path)
assert result == expected_hash assert result == expected_hash
@@ -101,63 +85,67 @@ def test_read_stored_hash_exists(tmp_path: Path) -> None:
hash_file = tmp_path / ".clang-tidy.hash" hash_file = tmp_path / ".clang-tidy.hash"
hash_file.write_text(f"{stored_hash}\n") hash_file.write_text(f"{stored_hash}\n")
result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path) with (
patch("clang_tidy_hash.Path") as mock_path_class,
patch("clang_tidy_hash.read_file_lines", return_value=[f"{stored_hash}\n"]),
):
# Mock the path calculation and exists check
mock_hash_file = Mock()
mock_hash_file.exists.return_value = True
mock_path_class.return_value.parent.parent.__truediv__.return_value = (
mock_hash_file
)
result = clang_tidy_hash.read_stored_hash()
assert result == stored_hash assert result == stored_hash
def test_read_stored_hash_not_exists(tmp_path: Path) -> None: def test_read_stored_hash_not_exists() -> None:
"""Test reading hash when file doesn't exist.""" """Test reading hash when file doesn't exist."""
result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path) with patch("clang_tidy_hash.Path") as mock_path_class:
# Mock the path calculation and exists check
mock_hash_file = Mock()
mock_hash_file.exists.return_value = False
mock_path_class.return_value.parent.parent.__truediv__.return_value = (
mock_hash_file
)
result = clang_tidy_hash.read_stored_hash()
assert result is None assert result is None
def test_write_hash(tmp_path: Path) -> None: def test_write_hash() -> None:
"""Test writing hash to file.""" """Test writing hash to file."""
hash_value = "abc123def456" hash_value = "abc123def456"
hash_file = tmp_path / ".clang-tidy.hash"
clang_tidy_hash.write_hash(hash_value, repo_root=tmp_path) with patch("clang_tidy_hash.write_file_content") as mock_write:
clang_tidy_hash.write_hash(hash_value)
assert hash_file.exists() # Verify write_file_content was called with correct parameters
assert hash_file.read_text() == hash_value.strip() + "\n" mock_write.assert_called_once()
args = mock_write.call_args[0]
assert str(args[0]).endswith(".clang-tidy.hash")
assert args[1] == hash_value.strip() + "\n"
@pytest.mark.parametrize( @pytest.mark.parametrize(
("args", "current_hash", "stored_hash", "hash_file_in_changed", "expected_exit"), ("args", "current_hash", "stored_hash", "expected_exit"),
[ [
(["--check"], "abc123", "abc123", False, 1), # Hashes match, no scan needed (["--check"], "abc123", "abc123", 1), # Hashes match, no scan needed
(["--check"], "abc123", "def456", False, 0), # Hashes differ, scan needed (["--check"], "abc123", "def456", 0), # Hashes differ, scan needed
(["--check"], "abc123", None, False, 0), # No stored hash, scan needed (["--check"], "abc123", None, 0), # No stored hash, scan needed
(
["--check"],
"abc123",
"abc123",
True,
0,
), # Hash file updated in PR, scan needed
], ],
) )
def test_main_check_mode( def test_main_check_mode(
args: list[str], args: list[str], current_hash: str, stored_hash: str | None, expected_exit: int
current_hash: str,
stored_hash: str | None,
hash_file_in_changed: bool,
expected_exit: int,
) -> None: ) -> None:
"""Test main function in check mode.""" """Test main function in check mode."""
changed = [".clang-tidy.hash"] if hash_file_in_changed else []
# Create a mock module that can be imported
mock_helpers = Mock()
mock_helpers.changed_files = Mock(return_value=changed)
with ( with (
patch("sys.argv", ["clang_tidy_hash.py"] + args), patch("sys.argv", ["clang_tidy_hash.py"] + args),
patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash),
patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash),
patch.dict("sys.modules", {"helpers": mock_helpers}),
pytest.raises(SystemExit) as exc_info, pytest.raises(SystemExit) as exc_info,
): ):
clang_tidy_hash.main() clang_tidy_hash.main()

View File

@@ -101,10 +101,3 @@ def mock_get_idedata() -> Generator[Mock, None, None]:
"""Mock get_idedata for platformio_api.""" """Mock get_idedata for platformio_api."""
with patch("esphome.platformio_api.get_idedata") as mock: with patch("esphome.platformio_api.get_idedata") as mock:
yield mock yield mock
@pytest.fixture
def mock_get_component() -> Generator[Mock, None, None]:
"""Mock get_component for config module."""
with patch("esphome.config.get_component") as mock:
yield mock

View File

@@ -1,10 +0,0 @@
esphome:
name: test-device
esp32:
board: esp32dev
# Test component with dynamic AUTO_LOAD
test_component:
enable_logger: true
enable_api: false

View File

@@ -1,8 +0,0 @@
esphome:
name: test-device
esp32:
board: esp32dev
# Test component with static AUTO_LOAD
test_component:

View File

@@ -1,131 +0,0 @@
"""Tests for AUTO_LOAD functionality including dynamic AUTO_LOAD."""
from pathlib import Path
from typing import Any
from unittest.mock import Mock
import pytest
from esphome import config, config_validation as cv, yaml_util
from esphome.core import CORE
@pytest.fixture
def fixtures_dir() -> Path:
"""Get the fixtures directory."""
return Path(__file__).parent / "fixtures"
@pytest.fixture
def default_component() -> Mock:
"""Create a default mock component for unmocked components."""
return Mock(
auto_load=[],
is_platform_component=False,
is_platform=False,
multi_conf=False,
multi_conf_no_default=False,
dependencies=[],
conflicts_with=[],
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
)
@pytest.fixture
def static_auto_load_component() -> Mock:
"""Create a mock component with static AUTO_LOAD."""
return Mock(
auto_load=["logger"],
is_platform_component=False,
is_platform=False,
multi_conf=False,
multi_conf_no_default=False,
dependencies=[],
conflicts_with=[],
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
)
def test_static_auto_load_adds_components(
mock_get_component: Mock,
fixtures_dir: Path,
static_auto_load_component: Mock,
default_component: Mock,
) -> None:
"""Test that static AUTO_LOAD triggers loading of specified components."""
CORE.config_path = fixtures_dir / "auto_load_static.yaml"
config_file = fixtures_dir / "auto_load_static.yaml"
raw_config = yaml_util.load_yaml(config_file)
component_mocks = {"test_component": static_auto_load_component}
mock_get_component.side_effect = lambda name: component_mocks.get(
name, default_component
)
result = config.validate_config(raw_config, {})
# Check for validation errors
assert not result.errors, f"Validation errors: {result.errors}"
# Logger should have been auto-loaded by test_component
assert "logger" in result
assert "test_component" in result
def test_dynamic_auto_load_with_config_param(
mock_get_component: Mock,
fixtures_dir: Path,
default_component: Mock,
) -> None:
"""Test that dynamic AUTO_LOAD evaluates based on configuration."""
CORE.config_path = fixtures_dir / "auto_load_dynamic.yaml"
config_file = fixtures_dir / "auto_load_dynamic.yaml"
raw_config = yaml_util.load_yaml(config_file)
# Track if auto_load was called with config
auto_load_calls = []
def dynamic_auto_load(conf: dict[str, Any]) -> list[str]:
"""Dynamically load components based on config."""
auto_load_calls.append(conf)
component_map = {
"enable_logger": "logger",
"enable_api": "api",
}
return [comp for key, comp in component_map.items() if conf.get(key)]
dynamic_component = Mock(
auto_load=dynamic_auto_load,
is_platform_component=False,
is_platform=False,
multi_conf=False,
multi_conf_no_default=False,
dependencies=[],
conflicts_with=[],
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
)
component_mocks = {"test_component": dynamic_component}
mock_get_component.side_effect = lambda name: component_mocks.get(
name, default_component
)
result = config.validate_config(raw_config, {})
# Check for validation errors
assert not result.errors, f"Validation errors: {result.errors}"
# Verify auto_load was called with the validated config
assert len(auto_load_calls) == 1, "auto_load should be called exactly once"
assert auto_load_calls[0].get("enable_logger") is True
assert auto_load_calls[0].get("enable_api") is False
# Only logger should be auto-loaded (enable_logger=true in YAML)
assert "logger" in result, (
f"Logger not found in result. Result keys: {list(result.keys())}"
)
# API should NOT be auto-loaded (enable_api=false in YAML)
assert "api" not in result
assert "test_component" in result

View File

@@ -10,6 +10,13 @@ from esphome import config, yaml_util
from esphome.core import CORE from esphome.core import CORE
@pytest.fixture
def mock_get_component() -> Generator[Mock, None, None]:
"""Fixture for mocking get_component."""
with patch("esphome.config.get_component") as mock_get_component:
yield mock_get_component
@pytest.fixture @pytest.fixture
def mock_get_platform() -> Generator[Mock, None, None]: def mock_get_platform() -> Generator[Mock, None, None]:
"""Fixture for mocking get_platform.""" """Fixture for mocking get_platform."""