diff --git a/.clang-tidy.hash b/.clang-tidy.hash index f61b79de4d..4901c0ccac 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 +049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 8760a1aaa5..78d1c2b87f 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -6,6 +6,7 @@ on: - ".clang-tidy" - "platformio.ini" - "requirements_dev.txt" + - "sdkconfig.defaults" - ".clang-tidy.hash" - "script/clang_tidy_hash.py" - ".github/workflows/ci-clang-tidy-hash.yml" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4f7f8bd82..bb038cb8aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,7 +466,7 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache env: SKIP: pylint,clang-tidy-hash - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5453dae9a7..59f58b7236 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f57f0987ec..63a8ade37f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 818f360860..521aaf9cc8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.13.2 + rev: v0.14.0 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 0b9935faf7..03ea5d0e47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/epaper_spi/* @esphome/core esphome/components/es7210/* @kahrendt esphome/components/es7243e/* @kbx81 esphome/components/es8156/* @kbx81 @@ -256,6 +257,7 @@ esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz +esphome/components/lm75b/* @beormund esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core @@ -428,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow +esphome/components/split_buffer/* @jesserockz esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 diff --git a/Doxyfile b/Doxyfile index cad97e645a..034fa3fa37 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.10.0-dev +PROJECT_NUMBER = 2025.11.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/__main__.py b/esphome/__main__.py index 55eaf59428..ab21142a3d 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -14,9 +14,11 @@ from typing import Protocol import argcomplete +# Note: Do not import modules from esphome.components here, as this would +# cause them to be loaded before external components are processed, resulting +# in the built-in version being used instead of the external component one. from esphome import const, writer, yaml_util import esphome.codegen as cg -from esphome.components.mqtt import CONF_DISCOVER_IP from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, @@ -240,6 +242,8 @@ def has_ota() -> bool: def has_mqtt_ip_lookup() -> bool: """Check if MQTT is available and IP lookup is supported.""" + from esphome.components.mqtt import CONF_DISCOVER_IP + if CONF_MQTT not in CORE.config: return False # Default Enabled diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 6db6f1a7bd..c2ae3b2f76 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f int Animation::get_current_frame() const { return this->current_frame_; } void Animation::next_frame() { this->current_frame_++; - if (loop_count_ && this->current_frame_ == loop_end_frame_ && + if (loop_count_ && static_cast(this->current_frame_) == loop_end_frame_ && (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { this->current_frame_ = loop_start_frame_; this->loop_current_iteration_++; } - if (this->current_frame_ >= animation_frame_count_) { + if (static_cast(this->current_frame_) >= animation_frame_count_) { this->loop_current_iteration_ = 1; this->current_frame_ = 0; } diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 1ee4c6f806..58828c131d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -9,37 +9,59 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ACTION, CONF_ACTIONS, + CONF_CAPTURE_RESPONSE, CONF_DATA, CONF_DATA_TEMPLATE, CONF_EVENT, CONF_ID, CONF_KEY, + CONF_MAX_CONNECTIONS, CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_ERROR, + CONF_ON_SUCCESS, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, + CONF_RESPONSE_TEMPLATE, CONF_SERVICE, CONF_SERVICES, CONF_TAG, CONF_TRIGGER_ID, CONF_VARIABLES, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import TemplateArgsType from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "api" DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket"] 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") APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) HomeAssistantServiceCallAction = api_ns.class_( "HomeAssistantServiceCallAction", automation.Action ) +ActionResponse = api_ns.class_("ActionResponse") +HomeAssistantActionResponseTrigger = api_ns.class_( + "HomeAssistantActionResponseTrigger", automation.Trigger +) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) @@ -60,7 +82,6 @@ CONF_CUSTOM_SERVICES = "custom_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" -CONF_MAX_CONNECTIONS = "max_connections" CONF_MAX_SEND_QUEUE = "max_send_queue" @@ -155,8 +176,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean, cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean, cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean, - cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean, - cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean, cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), @@ -290,6 +309,29 @@ async def to_code(config): 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( cv.Schema( { @@ -305,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( cv.Optional(CONF_VARIABLES, default={}): cv.Schema( {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.rename_key(CONF_SERVICE, CONF_ACTION), + _validate_response_config, ) @@ -322,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) -async def homeassistant_service_to_code(config, action_id, template_arg, args): +async def homeassistant_service_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) @@ -337,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args): for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) 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 diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0e385c4a17..87f477799d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -780,6 +780,22 @@ message HomeassistantActionRequest { repeated HomeassistantServiceMap data_template = 3; repeated HomeassistantServiceMap variables = 4; 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 ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2d12bf5f09..ae03dfbb33 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -8,9 +8,9 @@ #endif #include #include -#include #include #include +#include #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -116,8 +116,7 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Helper init failed"), err); + this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); return; } this->client_info_.peername = helper_->getpeername(); @@ -147,8 +146,7 @@ void APIConnection::loop() { APIError err = this->helper_->loop(); if (err != APIError::OK) { - on_fatal_error(); - this->log_socket_operation_failed_(err); + this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); return; } @@ -163,17 +161,13 @@ void APIConnection::loop() { // No more data available break; } else if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Reading failed"), err); + this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { this->last_traffic_ = now; // read a packet - if (buffer.data_len > 0) { - this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); - } else { - this->read_message(0, buffer.type, nullptr); - } + this->read_message(buffer.data_len, buffer.type, + buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); if (this->flags_.remove) return; } @@ -1395,6 +1389,11 @@ void APIConnection::complete_authentication_() { this->send_time_request(); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr) { + zwave_proxy::global_zwave_proxy->api_connection_authenticated(this); + } +#endif } bool APIConnection::send_hello_response(const HelloRequest &msg) { @@ -1550,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } } #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 bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; @@ -1580,8 +1593,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { - on_fatal_error(); - this->log_socket_operation_failed_(err); + this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); return false; } if (this->helper_->can_write_without_blocking()) @@ -1600,8 +1612,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { if (err == APIError::WOULD_BLOCK) return false; if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Packet write failed"), err); + this->fatal_error_with_log_(LOG_STR("Packet write failed"), err); return false; } // Do not set last_traffic_ on send @@ -1787,8 +1798,7 @@ void APIConnection::process_batch_() { APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf}, std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Batch write failed"), err); + this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1871,9 +1881,5 @@ void APIConnection::log_warning_(const LogString *message, APIError err) { LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } -void APIConnection::log_socket_operation_failed_(APIError err) { - this->log_warning_(LOG_STR("Socket operation failed"), err); -} - } // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a21574f6d5..284fa11a95 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection { return; this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); } -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; @@ -732,8 +735,11 @@ class APIConnection final : public APIServerConnection { // Helper function to log API errors with errno void log_warning_(const LogString *message, APIError err); - // Specific helper for duplicated error message - void log_socket_operation_failed_(APIError err); + // Helper to handle fatal errors with logging + inline void fatal_error_with_log_(const LogString *message, APIError err) { + this->on_fatal_error(); + this->log_warning_(message, err); + } }; } // namespace esphome::api diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 815064c973..9aaada3cf7 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -19,13 +19,14 @@ namespace esphome::api { //#define HELPER_LOG_PACKETS // Maximum message size limits to prevent OOM on constrained devices -// Voice Assistant is our largest user at 1024 bytes per audio chunk -// Using 2048 + 256 bytes overhead = 2304 bytes total to support voice and future needs -// ESP8266 has very limited RAM and cannot support voice assistant +// Handshake messages are limited to a small size for security +static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128; + +// Data message limits vary by platform based on available memory #ifdef USE_ESP8266 -static constexpr uint16_t MAX_MESSAGE_SIZE = 512; // Keep small for memory constrained ESP8266 +static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 #else -static constexpr uint16_t MAX_MESSAGE_SIZE = 2304; // Support voice (1024) + headroom for larger messages +static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif // Forward declaration diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index b265a2cf4d..1213e65948 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -132,26 +132,16 @@ APIError APINoiseFrameHelper::loop() { return APIFrameHelper::loop(); } -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter +/** Read a packet into the rx_buf_. * - * @param frame: The struct to hold the frame information in. - * msg_start: points to the start of the payload - this pointer is only valid until the next - * try_receive_raw_ call - * - * @return 0 if a full packet is in rx_buf_ - * @return -1 if error, check errno. + * @return APIError::OK if a full packet is in rx_buf_ * * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. * errno ENOMEM: Not enough memory for reading packet. * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. */ -APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - +APIError APINoiseFrameHelper::try_read_frame_() { // read header if (rx_header_buf_len_ < 3) { // no header information yet @@ -178,23 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { // read body uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; - if (state_ != State::DATA && msg_size > 128) { - // for handshake message only permit up to 128 bytes + // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data + uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; + if (msg_size > limit) { state_ = State::FAILED; - HELPER_LOG("Bad packet len for handshake: %d", msg_size); - return APIError::BAD_HANDSHAKE_PACKET_LEN; + HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit); + return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; } - // Check against maximum message size to prevent OOM - if (msg_size > MAX_MESSAGE_SIZE) { - state_ = State::FAILED; - HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, MAX_MESSAGE_SIZE); - return APIError::BAD_DATA_PACKET; - } - - // reserve space for body - if (rx_buf_.size() != msg_size) { - rx_buf_.resize(msg_size); + // Reserve space for body + if (this->rx_buf_.size() != msg_size) { + this->rx_buf_.resize(msg_size); } if (rx_buf_len_ < msg_size) { @@ -212,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { } } - LOG_PACKET_RECEIVED(rx_buf_); - *frame = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_len_ = 0; + LOG_PACKET_RECEIVED(this->rx_buf_); + + // Clear state for next frame (rx_buf_ still contains data for caller) + this->rx_buf_len_ = 0; + this->rx_header_buf_len_ = 0; + return APIError::OK; } @@ -239,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::CLIENT_HELLO) { // waiting for client hello - std::vector frame; - aerr = try_read_frame_(&frame); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { return handle_handshake_frame_error_(aerr); } // ignore contents, may be used in future for flags // Resize for: existing prologue + 2 size bytes + frame data - size_t old_size = prologue_.size(); - prologue_.resize(old_size + 2 + frame.size()); - prologue_[old_size] = (uint8_t) (frame.size() >> 8); - prologue_[old_size + 1] = (uint8_t) frame.size(); - std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size()); + size_t old_size = this->prologue_.size(); + this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); + this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); + this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); state_ = State::SERVER_HELLO; } @@ -292,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() { int action = noise_handshakestate_get_action(handshake_); if (action == NOISE_ACTION_READ_MESSAGE) { // waiting for handshake msg - std::vector frame; - aerr = try_read_frame_(&frame); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { return handle_handshake_frame_error_(aerr); } - if (frame.empty()) { + if (this->rx_buf_.empty()) { send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (frame[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", frame[0]); + } else if (this->rx_buf_[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); + noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); if (err != 0) { // Special handling for MAC failure @@ -386,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - int err; - APIError aerr; - aerr = state_action_(); + APIError aerr = this->state_action_(); if (aerr != APIError::OK) { return aerr; } - if (state_ != State::DATA) { + if (this->state_ != State::DATA) { return APIError::WOULD_BLOCK; } - std::vector frame; - aerr = try_read_frame_(&frame); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) return aerr; NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); - err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); + noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); + int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); APIError decrypt_err = 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; + } uint16_t msg_size = mbuf.size; - uint8_t *msg_data = frame.data(); + uint8_t *msg_data = this->rx_buf_.data(); if (msg_size < 4) { - state_ = State::FAILED; + this->state_ = State::FAILED; HELPER_LOG("Bad data packet: size %d too short", msg_size); return APIError::BAD_DATA_PACKET; } @@ -422,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { 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]; if (data_len > msg_size - 4) { - state_ = State::FAILED; + this->state_ = State::FAILED; HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(frame); + buffer->container = std::move(this->rx_buf_); buffer->data_offset = 4; buffer->data_len = data_len; buffer->type = type; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 71a217c4ca..e3243e4fa5 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper { protected: APIError state_action_(); - APIError try_read_frame_(std::vector *frame); + APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); APIError check_handshake_finished_(); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index f6024a87a1..471e6c5404 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -47,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() { return APIFrameHelper::loop(); } -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter - * - * @param frame: The struct to hold the frame information in. - * msg: store the parsed frame in that struct +/** Read a packet into the rx_buf_. * * @return See APIError * * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. */ -APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - +APIError APIPlaintextFrameHelper::try_read_frame_() { // read header while (!rx_header_parsed_) { // Now that we know when the socket is ready, we can read up to 3 bytes @@ -150,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { } // header reading done - // reserve space for body - if (rx_buf_.size() != rx_header_parsed_len_) { - rx_buf_.resize(rx_header_parsed_len_); + // Reserve space for body + if (this->rx_buf_.size() != this->rx_header_parsed_len_) { + this->rx_buf_.resize(this->rx_header_parsed_len_); } if (rx_buf_len_ < rx_header_parsed_len_) { @@ -170,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { } } - LOG_PACKET_RECEIVED(rx_buf_); - *frame = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_pos_ = 0; - rx_header_parsed_ = false; + LOG_PACKET_RECEIVED(this->rx_buf_); + + // Clear state for next frame (rx_buf_ still contains data for caller) + this->rx_buf_len_ = 0; + this->rx_header_buf_pos_ = 0; + this->rx_header_parsed_ = false; + return APIError::OK; } -APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr; - if (state_ != State::DATA) { +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + if (this->state_ != State::DATA) { return APIError::WOULD_BLOCK; } - std::vector frame; - aerr = try_read_frame_(&frame); + APIError aerr = this->try_read_frame_(); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { // Make sure to tell the remote that we don't @@ -220,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return aerr; } - buffer->container = std::move(frame); + buffer->container = std::move(this->rx_buf_); buffer->data_offset = 0; - buffer->data_len = rx_header_parsed_len_; - buffer->type = rx_header_parsed_type_; + buffer->data_len = this->rx_header_parsed_len_; + buffer->type = this->rx_header_parsed_type_; return APIError::OK; } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 55a6d0f744..bba981d26b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper { APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; protected: - APIError try_read_frame_(std::vector *frame); + APIError try_read_frame_(); // Group 2-byte aligned types uint16_t rx_header_parsed_type_ = 0; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0140c60e5b..70bcf082a6 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(4, it, true); } 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 { size.add_length(1, this->service_ref_.size()); @@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->variables); 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 #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d71ee9777d..d9e68ece9b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 35; - static constexpr uint8_t ESTIMATED_SIZE = 113; + static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_action_request"; } #endif @@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage { std::vector data_template{}; std::vector variables{}; 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 calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage { protected: }; #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 class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { public: diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index c5f1d99dd4..cf732e451b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const { out.append("\n"); } 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 #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index ccbd781431..9d227af0a3 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_z_wave_proxy_request(msg); 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 default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1afcba6664..549b00ee6a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService { virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; #endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; +#endif #ifdef USE_API_HOMEASSISTANT_STATES virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a8fdb635cf..778d9389ef 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -9,12 +9,16 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" #include "esphome/core/version.h" +#ifdef USE_API_HOMEASSISTANT_SERVICES +#include "homeassistant_service.h" +#endif #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif #include +#include namespace esphome::api { @@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call client->send_homeassistant_action(call); } } -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +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 void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index b9049c1700..5d038e5ddd 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -16,6 +16,7 @@ #include "user_services.h" #endif +#include #include namespace esphome::api { @@ -111,7 +112,17 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + // Action response handling + using ActionResponseCallback = std::function; + 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 void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif @@ -187,6 +198,13 @@ class APIServer : public Component, public Controller { #ifdef USE_API_SERVICES std::vector user_services_; #endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + struct PendingActionResponse { + uint32_t call_id; + ActionResponseCallback callback; + }; + std::vector action_response_callbacks_; +#endif // Group smaller types together uint16_t port_{6053}; diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 4026741ee4..730024f7b7 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -3,8 +3,13 @@ #include "api_server.h" #ifdef USE_API #ifdef USE_API_HOMEASSISTANT_SERVICES +#include +#include #include #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/helpers.h" @@ -44,9 +49,47 @@ template class TemplatableKeyValuePair { TemplatableStringValue 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(); } +#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 using ActionResponseCallback = std::function; +#endif + template class HomeAssistantServiceCallAction : public Action { public: - explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} + explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) { + this->flags_.is_event = is_event; + } template void set_service(T service) { this->service_ = service; } @@ -61,11 +104,29 @@ template class HomeAssistantServiceCallAction : public Actionvariables_.emplace_back(std::move(key), value); } +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + template 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 *get_success_trigger_with_response() const { + return this->success_trigger_with_response_; + } +#endif + Trigger *get_success_trigger() const { return this->success_trigger_; } + Trigger *get_error_trigger() const { return this->error_trigger_; } +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES + void play(Ts... x) override { HomeassistantActionRequest resp; std::string service_value = this->service_.value(x...); resp.set_service(StringRef(service_value)); - resp.is_event = this->is_event_; + resp.is_event = this->flags_.is_event; for (auto &it : this->data_) { resp.data.emplace_back(); auto &kv = resp.data.back(); @@ -84,18 +145,74 @@ template class HomeAssistantServiceCallAction : public Actionflags_.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); } protected: APIServer *parent_; - bool is_event_; TemplatableStringValue service_{}; std::vector> data_; std::vector> data_template_; std::vector> variables_; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + TemplatableStringValue response_template_{""}; + Trigger *success_trigger_with_response_ = new Trigger(); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + Trigger *success_trigger_ = new Trigger(); + Trigger *error_trigger_ = new Trigger(); +#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 + #endif #endif diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index dba2d055bf..3996c921a9 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -35,7 +35,7 @@ template class UserServiceBase : public UserServiceDescriptor { msg.set_name(StringRef(this->name_)); msg.key = this->key_; std::array arg_types = {to_service_arg_type()...}; - for (int i = 0; i < sizeof...(Ts); i++) { + for (size_t i = 0; i < sizeof...(Ts); i++) { msg.args.emplace_back(); auto &arg = msg.args.back(); arg.type = arg_types[i]; diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index f657cb5da3..7b03e4b6a7 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -165,4 +165,4 @@ def final_validate_audio_schema( async def to_code(config): - cg.add_library("esphome/esp-audio-libs", "1.1.4") + cg.add_library("esphome/esp-audio-libs", "2.0.1") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 2a58c38ac7..9cc9b7d0da 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) { void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale) { // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. - for (int i = 0; i < samples_to_scale; i++) { + for (size_t i = 0; i < samples_to_scale; i++) { int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor; output_buffer[i] = (int16_t) (acc >> 15); } diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 90ba1aec1e..d1ad571a52 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() { auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); - if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - return FileDecoderState::POTENTIALLY_FAILED; - } - - if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) { - // Couldn't read FLAC header + if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { + // Serrious error reading FLAC header, there is no recovery return FileDecoderState::FAILED; } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { + return FileDecoderState::MORE_TO_PROCESS; + } + // Reallocate the output transfer buffer to the smallest necessary size this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { @@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() { } uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), - reinterpret_cast(this->output_transfer_buffer_->get_buffer_end()), &output_samples); + auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(), + this->input_transfer_buffer_->available(), + this->output_transfer_buffer_->get_buffer_end(), &output_samples); if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { // Not an issue, just needs more data that we'll get next time. diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index e48715010c..c1cd48a1ac 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -97,10 +97,10 @@ void BL0906::handle_actions_() { return; } ActionCallbackFuncPtr ptr_func = nullptr; - for (int i = 0; i < this->action_queue_.size(); i++) { + for (size_t i = 0; i < this->action_queue_.size(); i++) { ptr_func = this->action_queue_[i]; if (ptr_func) { - ESP_LOGI(TAG, "HandleActionCallback[%d]", i); + ESP_LOGI(TAG, "HandleActionCallback[%zu]", i); (this->*ptr_func)(); } } diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 894fcbfbb7..95dd689b07 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -51,7 +51,7 @@ void BL0942::loop() { if (!avail) { return; } - if (avail < sizeof(buffer)) { + if (static_cast(avail) < sizeof(buffer)) { if (!this->rx_start_) { this->rx_start_ = millis(); } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { @@ -148,7 +148,7 @@ void BL0942::setup() { this->write_reg_(BL0942_REG_USR_WRPROT, 0); - if (this->read_reg_(BL0942_REG_MODE) != mode) + if (static_cast(this->read_reg_(BL0942_REG_MODE)) != mode) this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->flush(); diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 5f4ea8afd1..768a345213 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.COMPONENT_SCHEMA) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA), - esp32_ble_tracker.consume_connection_slots(1, "ble_client"), + esp32_ble.consume_connection_slots(1, "ble_client"), ) CONF_BLE_CLIENT_ID = "ble_client_id" diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 42a88f1421..ad7528c156 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -42,9 +42,7 @@ def validate_connections(config): ) elif config[CONF_ACTIVE]: connection_slots: int = config[CONF_CONNECTION_SLOTS] - esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( - config - ) + esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config) return { **config, @@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All( default=DEFAULT_CONNECTION_SLOTS, ): cv.All( cv.positive_int, - cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS), + cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS), ), cv.Optional(CONF_CONNECTIONS): cv.All( cv.ensure_list(CONNECTION_SCHEMA), - cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS), + cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS), ), } ) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 20abc6506d..30438747f2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -11,14 +11,14 @@ namespace captive_portal { static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); - stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); + AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); + stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); #ifdef USE_ESP8266 - stream->print(F("{\"mac\":\"")); + stream->print(ESPHOME_F("{\"mac\":\"")); stream->print(get_mac_address_pretty().c_str()); - stream->print(F("\",\"name\":\"")); + stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(App.get_name().c_str()); - stream->print(F("\",\"aps\":[{}")); + stream->print(ESPHOME_F("\",\"aps\":[{}")); #else stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); #endif @@ -29,19 +29,19 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { // Assumes no " in ssid, possible unicode isses? #ifdef USE_ESP8266 - stream->print(F(",{\"ssid\":\"")); + stream->print(ESPHOME_F(",{\"ssid\":\"")); stream->print(scan.get_ssid().c_str()); - stream->print(F("\",\"rssi\":")); + stream->print(ESPHOME_F("\",\"rssi\":")); stream->print(scan.get_rssi()); - stream->print(F(",\"lock\":")); + stream->print(ESPHOME_F(",\"lock\":")); stream->print(scan.get_with_auth()); - stream->print(F("}")); + stream->print(ESPHOME_F("}")); #else stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), scan.get_with_auth()); #endif } - stream->print(F("]}")); + stream->print(ESPHOME_F("]}")); request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { @@ -52,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - request->redirect(F("/?save")); + request->redirect(ESPHOME_F("/?save")); } void CaptivePortal::setup() { @@ -75,7 +75,7 @@ void CaptivePortal::start() { #ifdef USE_ARDUINO this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); - this->dns_server_->start(53, F("*"), ip); + this->dns_server_->start(53, ESPHOME_F("*"), ip); #endif this->initialized_ = true; @@ -88,10 +88,10 @@ void CaptivePortal::start() { } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == F("/config.json")) { + if (req->url() == ESPHOME_F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == F("/wifisave")) { + } else if (req->url() == ESPHOME_F("/wifisave")) { this->handle_wifisave(req); return; } @@ -100,11 +100,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { // This includes OS captive portal detection endpoints which will trigger // the captive portal when they don't receive their expected responses #ifndef USE_ESP8266 - auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #else - auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #endif - response->addHeader(F("Content-Encoding"), F("gzip")); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); req->send(response); } diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp index 339a1659ac..d88ea2e1da 100644 --- a/esphome/components/cm1106/cm1106.cpp +++ b/esphome/components/cm1106/cm1106.cpp @@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, uint8_t cm1106_checksum(const uint8_t *response, size_t len) { uint8_t crc = 0; - for (int i = 0; i < len - 1; i++) { + for (size_t i = 0; i < len - 1; i++) { crc -= response[i]; } return crc; diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index f806463d00..068819ecd1 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() { uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00}; // Calculate checksum - for (int i = 0; i < sizeof(remote_header) - 1; i++) { + for (size_t i = 0; i < sizeof(remote_header) - 1; i++) { remote_header[sizeof(remote_header) - 1] += remote_header[i]; } @@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() { remote_state[9] = fan_speed & 0xff; // Calculate checksum - for (int i = 0; i < sizeof(remote_header) - 1; i++) { + for (size_t i = 0; i < sizeof(remote_header) - 1; i++) { remote_header[sizeof(remote_header) - 1] += remote_header[i]; } @@ -350,7 +350,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { bool valid_daikin_frame = false; if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; - int bytes_count = data.size() / 2 / 8; + size_t bytes_count = data.size() / 2 / 8; std::unique_ptr buf(new char[bytes_count * 3 + 1]); buf[0] = '\0'; for (size_t i = 0; i < bytes_count; i++) { @@ -370,7 +370,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (!valid_daikin_frame) { char sbuf[16 * 10 + 1]; sbuf[0] = '\0'; - for (size_t j = 0; j < data.size(); j++) { + for (size_t j = 0; j < static_cast(data.size()); j++) { if ((j - 2) % 16 == 0) { if (j > 0) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); @@ -380,19 +380,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { char type_ch = ' '; // debug_tolerance = 25% - if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))) type_ch = 'P'; - if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))) type_ch = 'a'; - if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))) type_ch = 'H'; - if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))) type_ch = 'h'; - if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))) type_ch = 'B'; - if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))) type_ch = '1'; - if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))) type_ch = '0'; if (abs(data[j]) > 100000) { @@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { } else { sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); } - if (j == data.size() - 1) { + if (j + 1 == static_cast(data.size())) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); } } diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index 6875fd61a5..c04696fd53 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -5,7 +5,7 @@ namespace dashboard_import { static std::string g_package_import_url; // NOLINT -std::string get_package_import_url() { return g_package_import_url; } +const std::string &get_package_import_url() { return g_package_import_url; } void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } } // namespace dashboard_import diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index 0ca2994aab..edcda6b803 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -5,7 +5,7 @@ namespace esphome { namespace dashboard_import { -std::string get_package_import_url(); +const std::string &get_package_import_url(); void set_package_import_url(std::string url); } // namespace dashboard_import diff --git a/esphome/components/epaper_spi/__init__.py b/esphome/components/epaper_spi/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/epaper_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py new file mode 100644 index 0000000000..20549f049d --- /dev/null +++ b/esphome/components/epaper_spi/display.py @@ -0,0 +1,80 @@ +from esphome import core, pins +import esphome.codegen as cg +from esphome.components import display, spi +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUSY_PIN, + CONF_DC_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_MODEL, + CONF_PAGES, + CONF_RESET_DURATION, + CONF_RESET_PIN, +) + +AUTO_LOAD = ["split_buffer"] +DEPENDENCIES = ["spi"] + +epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") +EPaperBase = epaper_spi_ns.class_( + "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) + +EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) +EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) + +MODELS = { + "7.3in-spectra-e6": EPaper7p3InSpectraE6, +} + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EPaperBase), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_RESET_DURATION): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "epaper_spi", require_miso=False, require_mosi=True +) + + +async def to_code(config): + model = MODELS[config[CONF_MODEL]] + + rhs = model.new() + var = cg.Pvariable(config[CONF_ID], rhs, model) + + await display.register_display(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BUSY_PIN in config: + busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + cg.add(var.set_busy_pin(busy)) + if CONF_RESET_DURATION in config: + cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp new file mode 100644 index 0000000000..21be4a2c05 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -0,0 +1,227 @@ +#include "epaper_spi.h" +#include +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static const char *const TAG = "epaper_spi"; + +static const LogString *epaper_state_to_string(EPaperState state) { + switch (state) { + case EPaperState::IDLE: + return LOG_STR("IDLE"); + case EPaperState::UPDATE: + return LOG_STR("UPDATE"); + case EPaperState::RESET: + return LOG_STR("RESET"); + case EPaperState::INITIALISE: + return LOG_STR("INITIALISE"); + case EPaperState::TRANSFER_DATA: + return LOG_STR("TRANSFER_DATA"); + case EPaperState::POWER_ON: + return LOG_STR("POWER_ON"); + case EPaperState::REFRESH_SCREEN: + return LOG_STR("REFRESH_SCREEN"); + case EPaperState::POWER_OFF: + return LOG_STR("POWER_OFF"); + case EPaperState::DEEP_SLEEP: + return LOG_STR("DEEP_SLEEP"); + default: + return LOG_STR("UNKNOWN"); + } +} + +void EPaperBase::setup() { + if (!this->init_buffer_(this->get_buffer_length())) { + this->mark_failed("Failed to initialise buffer"); + return; + } + this->setup_pins_(); + this->spi_setup(); +} + +bool EPaperBase::init_buffer_(size_t buffer_length) { + if (!this->buffer_.init(buffer_length)) { + return false; + } + this->clear(); + return true; +} + +void EPaperBase::setup_pins_() { + this->dc_pin_->setup(); // OUTPUT + this->dc_pin_->digital_write(false); + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); // OUTPUT + this->reset_pin_->digital_write(true); + } + + if (this->busy_pin_ != nullptr) { + this->busy_pin_->setup(); // INPUT + } +} + +float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void EPaperBase::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void EPaperBase::data(uint8_t value) { + this->start_data_(); + this->write_byte(value); + this->end_data_(); +} + +// write a command followed by zero or more bytes of data. +// The command is the first byte, length is the length of data only in the second byte, followed by the data. +// [COMMAND, LENGTH, DATA...] +void EPaperBase::cmd_data(const uint8_t *data) { + const uint8_t command = data[0]; + const uint8_t length = data[1]; + const uint8_t *ptr = data + 2; + + ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, + format_hex_pretty(ptr, length, '.', false).c_str()); + + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(command); + if (length > 0) { + this->dc_pin_->digital_write(true); + this->write_array(ptr, length); + } + this->disable(); +} + +bool EPaperBase::is_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + return !this->busy_pin_->digital_read(); +} + +void EPaperBase::reset() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + this->disable_loop(); + this->set_timeout(this->reset_duration_, [this] { + this->reset_pin_->digital_write(true); + this->set_timeout(20, [this] { this->enable_loop(); }); + }); + } +} + +void EPaperBase::update() { + if (!this->state_queue_.empty()) { + ESP_LOGE(TAG, "Display update already in progress - %s", + LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front()))); + return; + } + + this->state_queue_.push(EPaperState::UPDATE); + this->state_queue_.push(EPaperState::RESET); + this->state_queue_.push(EPaperState::INITIALISE); + this->state_queue_.push(EPaperState::TRANSFER_DATA); + this->state_queue_.push(EPaperState::POWER_ON); + this->state_queue_.push(EPaperState::REFRESH_SCREEN); + this->state_queue_.push(EPaperState::POWER_OFF); + this->state_queue_.push(EPaperState::DEEP_SLEEP); + this->state_queue_.push(EPaperState::IDLE); + + this->enable_loop(); +} + +void EPaperBase::loop() { + if (this->waiting_for_idle_) { + if (this->is_idle_()) { + this->waiting_for_idle_ = false; + } else { + if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) { + ESP_LOGV(TAG, "Waiting for idle"); + this->waiting_for_idle_last_print_ = App.get_loop_component_start_time(); + } + return; + } + } + + auto state = this->state_queue_.front(); + + switch (state) { + case EPaperState::IDLE: + this->disable_loop(); + break; + case EPaperState::UPDATE: + this->do_update_(); // Calls ESPHome (current page) lambda + break; + case EPaperState::RESET: + this->reset(); + break; + case EPaperState::INITIALISE: + this->initialise_(); + break; + case EPaperState::TRANSFER_DATA: + if (!this->transfer_data()) { + return; // Not done yet, come back next loop + } + break; + case EPaperState::POWER_ON: + this->power_on(); + break; + case EPaperState::REFRESH_SCREEN: + this->refresh_screen(); + break; + case EPaperState::POWER_OFF: + this->power_off(); + break; + case EPaperState::DEEP_SLEEP: + this->deep_sleep(); + break; + } + this->state_queue_.pop(); +} + +void EPaperBase::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} + +void EPaperBase::end_command_() { this->disable(); } + +void EPaperBase::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} +void EPaperBase::end_data_() { this->disable(); } + +void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } + +void EPaperBase::initialise_() { + size_t index = 0; + const auto &sequence = this->init_sequence_; + const size_t sequence_size = this->init_sequence_length_; + while (index != sequence_size) { + if (sequence_size - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + const auto *ptr = sequence + index; + const uint8_t length = ptr[1]; + if (sequence_size - index < length + 2) { + this->mark_failed("Malformed init sequence"); + return; + } + + this->cmd_data(ptr); + index += length + 2; + } + + this->power_on(); +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h new file mode 100644 index 0000000000..f6b2d41c65 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -0,0 +1,93 @@ +#pragma once + +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/split_buffer/split_buffer.h" +#include "esphome/core/component.h" + +#include + +namespace esphome::epaper_spi { + +enum class EPaperState : uint8_t { + IDLE, + UPDATE, + RESET, + INITIALISE, + TRANSFER_DATA, + POWER_ON, + REFRESH_SCREEN, + POWER_OFF, + DEEP_SLEEP, +}; + +static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run + +class EPaperBase : public display::DisplayBuffer, + public spi::SPIDevice { + public: + EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) + : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + float get_setup_priority() const override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } + void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } + + void command(uint8_t value); + void data(uint8_t value); + void cmd_data(const uint8_t *data); + + void update() override; + void loop() override; + + void setup() override; + + void on_safe_shutdown() override; + + protected: + bool is_idle_(); + void setup_pins_(); + virtual void reset(); + void initialise_(); + bool init_buffer_(size_t buffer_length); + + virtual int get_width_controller() { return this->get_width_internal(); }; + virtual void deep_sleep() = 0; + /** + * Send data to the device via SPI + * @return true if done, false if should be called next loop + */ + virtual bool transfer_data() = 0; + virtual void refresh_screen() = 0; + + virtual void power_on() = 0; + virtual void power_off() = 0; + virtual uint32_t get_buffer_length() = 0; + + void start_command_(); + void end_command_(); + void start_data_(); + void end_data_(); + + const size_t init_sequence_length_{0}; + + size_t current_data_index_{0}; + uint32_t reset_duration_{200}; + uint32_t waiting_for_idle_last_print_{0}; + + GPIOPin *dc_pin_; + GPIOPin *busy_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + + const uint8_t *init_sequence_{nullptr}; + + bool waiting_for_idle_{false}; + + split_buffer::SplitBuffer buffer_; + + std::queue state_queue_{{EPaperState::IDLE}}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp new file mode 100644 index 0000000000..f6273b392f --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp @@ -0,0 +1,42 @@ +#include "epaper_spi_model_7p3in_spectra_e6.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6"; + +void EPaper7p3InSpectraE6::power_on() { + ESP_LOGI(TAG, "Power on"); + this->command(0x04); + this->waiting_for_idle_ = true; +} + +void EPaper7p3InSpectraE6::power_off() { + ESP_LOGI(TAG, "Power off"); + this->command(0x02); + this->data(0x00); + this->waiting_for_idle_ = true; +} + +void EPaper7p3InSpectraE6::refresh_screen() { + ESP_LOGI(TAG, "Refresh"); + this->command(0x12); + this->data(0x00); + this->waiting_for_idle_ = true; +} + +void EPaper7p3InSpectraE6::deep_sleep() { + ESP_LOGI(TAG, "Deep sleep"); + this->command(0x07); + this->data(0xA5); +} + +void EPaper7p3InSpectraE6::dump_config() { + LOG_DISPLAY("", "E-Paper SPI", this); + ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h new file mode 100644 index 0000000000..6e850085ac --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h @@ -0,0 +1,45 @@ +#pragma once + +#include "epaper_spi_spectra_e6.h" + +namespace esphome::epaper_spi { + +class EPaper7p3InSpectraE6 : public EPaperSpectraE6 { + static constexpr const uint16_t WIDTH = 800; + static constexpr const uint16_t HEIGHT = 480; + // clang-format off + + // Command, data length, data + static constexpr uint8_t INIT_SEQUENCE[] = { + 0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18, + 0x01, 1, 0x3F, + 0x00, 2, 0x5F, 0x69, + 0x03, 4, 0x00, 0x54, 0x00, 0x44, + 0x05, 4, 0x40, 0x1F, 0x1F, 0x2C, + 0x06, 4, 0x6F, 0x1F, 0x17, 0x49, + 0x08, 4, 0x6F, 0x1F, 0x1F, 0x22, + 0x30, 1, 0x03, + 0x50, 1, 0x3F, + 0x60, 2, 0x02, 0x00, + 0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256, + 0x84, 1, 0x01, + 0xE3, 1, 0x2F, + }; + // clang-format on + + public: + EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {} + + void dump_config() override; + + protected: + int get_width_internal() override { return WIDTH; }; + int get_height_internal() override { return HEIGHT; }; + + void refresh_screen() override; + void power_on() override; + void power_off() override; + void deep_sleep() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp new file mode 100644 index 0000000000..dccc691252 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -0,0 +1,135 @@ +#include "epaper_spi_spectra_e6.h" + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_spi.6c"; + +static inline uint8_t color_to_hex(Color color) { + if (color.red > 127) { + if (color.green > 170) { + if (color.blue > 127) { + return 0x1; // White + } else { + return 0x2; // Yellow + } + } else { + return 0x3; // Red (or Magenta) + } + } else { + if (color.green > 127) { + if (color.blue > 127) { + return 0x5; // Cyan -> Blue + } else { + return 0x6; // Green + } + } else { + if (color.blue > 127) { + return 0x5; // Blue + } else { + return 0x0; // Black + } + } + } +} + +void EPaperSpectraE6::fill(Color color) { + uint8_t pixel_color; + if (color.is_on()) { + pixel_color = color_to_hex(color); + } else { + pixel_color = 0x1; + } + + // We store 8 bitset<3> in 3 bytes + // | byte 1 | byte 2 | byte 3 | + // |aaabbbaa|abbbaaab|bbaaabbb| + uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1; + uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2; + uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0; + + const size_t buffer_length = this->get_buffer_length(); + for (size_t i = 0; i < buffer_length; i += 3) { + this->buffer_[i + 0] = byte_1; + this->buffer_[i + 1] = byte_2; + this->buffer_[i + 2] = byte_3; + } +} + +uint32_t EPaperSpectraE6::get_buffer_length() { + // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes + return this->get_width_controller() * this->get_height_internal() / 8u * 3u; +} + +void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + return; + + uint8_t pixel_bits = color_to_hex(color); + uint32_t pixel_position = x + y * this->get_width_controller(); + uint32_t first_bit_position = pixel_position * 3; + uint32_t byte_position = first_bit_position / 8u; + uint32_t byte_subposition = first_bit_position % 8u; + + if (byte_subposition <= 5) { + this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) | + (pixel_bits << (5 - byte_subposition)); + } else { + this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | + (pixel_bits >> (byte_subposition - 5)); + + this->buffer_[byte_position + 1] = + (this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) | + (pixel_bits << (13 - byte_subposition)); + } +} + +bool HOT EPaperSpectraE6::transfer_data() { + const uint32_t start_time = App.get_loop_component_start_time(); + if (this->current_data_index_ == 0) { + ESP_LOGV(TAG, "Sending data"); + this->command(0x10); + } + + uint8_t bytes_to_send[4]{0}; + const size_t buffer_length = this->get_buffer_length(); + for (size_t i = this->current_data_index_; i < buffer_length; i += 3) { + const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); + // 8 pixels are stored in 3 bytes + // |aaabbbaa|abbbaaab|bbaaabbb| + // | byte 1 | byte 2 | byte 3 | + bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111); + bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111); + bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111); + bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111); + + this->start_data_(); + this->write_array(bytes_to_send, sizeof(bytes_to_send)); + this->end_data_(); + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->current_data_index_ = i + 3; + return false; + } + } + // Finished the entire dataset + this->current_data_index_ = 0; + return true; +} + +void EPaperSpectraE6::reset() { + if (this->reset_pin_ != nullptr) { + this->disable_loop(); + this->reset_pin_->digital_write(true); + this->set_timeout(20, [this] { + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + this->set_timeout(20, [this] { this->enable_loop(); }); + }); + } +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h new file mode 100644 index 0000000000..9f0652f79d --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -0,0 +1,23 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +class EPaperSpectraE6 : public EPaperBase { + public: + EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length) + : EPaperBase(init_sequence, init_sequence_length) {} + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void fill(Color color) override; + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length() override; + + bool transfer_data() override; + void reset() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index e5729703ed..1358121c1b 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) { } bool ES7210::configure_sample_rate_() { - int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; + uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; int coeff = -1; - for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { + for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre) - coeff = i; + coeff = static_cast(i); } if (coeff >= 0) { diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f5eda52cae..860f2450e6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" -def _format_framework_espidf_version( - ver: cv.Version, release: str, for_platformio: bool -) -> str: - # format the given arduino (https://github.com/espressif/esp-idf/releases) version to +def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: + # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf - if for_platformio: - return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" if release: return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" @@ -317,157 +312,114 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) -# The platform-espressif32 version to use for arduino frameworks -# - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ARDUINO_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(3, 2, 1), + "latest": cv.Version(3, 3, 1), + "dev": cv.Version(3, 3, 1), +} +ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 1): cv.Version(55, 3, 31), + cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), + cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), + cv.Version(3, 2, 0): cv.Version(54, 3, 20), + cv.Version(3, 1, 3): cv.Version(53, 3, 13), + cv.Version(3, 1, 2): cv.Version(53, 3, 12), + cv.Version(3, 1, 1): cv.Version(53, 3, 11), + cv.Version(3, 1, 0): cv.Version(53, 3, 10), +} # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases -# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) -# The platformio/espressif32 version to use for esp-idf frameworks -# - https://github.com/platformio/platform-espressif32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(5, 4, 2), + "latest": cv.Version(5, 5, 1), + "dev": cv.Version(5, 5, 1), +} +ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 1): cv.Version(55, 3, 31), + cv.Version(5, 5, 0): cv.Version(55, 3, 31), + cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), + cv.Version(5, 3, 2): cv.Version(53, 3, 13), + cv.Version(5, 3, 1): cv.Version(53, 3, 13), + cv.Version(5, 3, 0): cv.Version(53, 3, 13), + cv.Version(5, 1, 6): cv.Version(51, 3, 7), + cv.Version(5, 1, 5): cv.Version(51, 3, 7), +} -# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions -SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 2, 2), - cv.Version(5, 2, 1), - cv.Version(5, 1, 2), - cv.Version(5, 1, 1), - cv.Version(5, 1, 0), - cv.Version(5, 0, 2), - cv.Version(5, 0, 1), - cv.Version(5, 0, 0), -] - -# pioarduino versions that don't require a release number -# List based on https://github.com/pioarduino/esp-idf/releases -SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ - cv.Version(5, 5, 1), - cv.Version(5, 5, 0), - cv.Version(5, 4, 2), - cv.Version(5, 4, 1), - cv.Version(5, 4, 0), - cv.Version(5, 3, 3), - cv.Version(5, 3, 2), - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 1, 5), - cv.Version(5, 1, 6), -] +# The platform-espressif32 version +# - https://github.com/pioarduino/platform-espressif32/releases +PLATFORM_VERSION_LOOKUP = { + "recommended": cv.Version(54, 3, 21, "2"), + "latest": cv.Version(55, 3, 31), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", +} def _check_versions(value): value = value.copy() - if value[CONF_TYPE] == FRAMEWORK_ARDUINO: - lookups = { - "dev": ( - cv.Version(3, 2, 1), - "https://github.com/espressif/arduino-esp32.git", - ), - "latest": (cv.Version(3, 2, 1), None), - "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), - } - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: - raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." - ) - - version, source = lookups[value[CONF_VERSION]] - else: - version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, - _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)), - ) - - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}" - - if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: - _LOGGER.warning( - "The selected Arduino framework version is not the recommended one. " - "If there are connectivity or build issues please remove the manual version." - ) - - return value - - lookups = { - "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 2, 2), None), - "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), - } - - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: + if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: + if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." + "Version needs to be explicitly set when a custom source or platform_version is used." ) - version, source = lookups[value[CONF_VERSION]] + platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]] + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] + else: + version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] else: version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - if version < cv.Version(5, 0, 0): - raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") - - # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below - has_platform_ver = CONF_PLATFORM_VERSION in value - - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) - ) - - if ( - is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION]) - ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X: - raise cv.Invalid( - f"ESP-IDF {str(version)} not supported by platformio/espressif32" - ) - - if ( - version in SUPPORTED_PLATFORMIO_ESP_IDF_5X - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ) and not has_platform_ver: - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'" - ) - - if ( - not is_platformio - and CONF_RELEASE not in value - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ): - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'" - ) value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_espidf_version( - version, value.get(CONF_RELEASE, None), is_platformio - ) - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}" + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + if version < cv.Version(3, 0, 0): + raise cv.Invalid("Only Arduino 3.0+ is supported.") + recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, _format_framework_arduino_version(version) + ) + if value[CONF_SOURCE].startswith("http"): + value[CONF_SOURCE] = ( + f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" + ) + else: + if version < cv.Version(5, 0, 0): + raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") + recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, + _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + ) + if value[CONF_SOURCE].startswith("http"): + value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" - if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + if CONF_PLATFORM_VERSION not in value: + if platform_lookup is None: + raise cv.Invalid( + "Framework version not recognized; please specify platform_version" + ) + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if version != recommended_version: _LOGGER.warning( - "The selected ESP-IDF framework version is not the recommended one. " + "The selected framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." + ) + + if value[CONF_PLATFORM_VERSION] != _parse_platform_version( + str(PLATFORM_VERSION_LOOKUP["recommended"]) + ): + _LOGGER.warning( + "The selected platform version is not the recommended one. " "If there are connectivity or build issues please remove the manual version." ) @@ -477,26 +429,14 @@ def _check_versions(value): def _parse_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) - if ver.major >= 50: # a pioarduino version - release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" - if ver.extra: - release += f"-{ver.extra}" - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" - # if platform version is a valid version constraint, prefix the default package - cv.platformio_version_constraint(value) - return f"platformio/espressif32@{value}" + release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + release += f"-{ver.extra}" + return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" except cv.Invalid: return value -def _platform_is_platformio(value): - try: - ver = cv.Version.parse(cv.version_number(value)) - return ver.major < 50 - except cv.Invalid: - return "platformio" in value - - def _detect_variant(value): board = value.get(CONF_BOARD) variant = value.get(CONF_VARIANT) @@ -808,6 +748,8 @@ async def to_code(config): conf = config[CONF_FRAMEWORK] cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + if CONF_SOURCE in conf: + cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") @@ -850,8 +792,6 @@ async def to_code(config): cg.add_build_flag("-Wno-nonnull-compare") - cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) - add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 0501d1c5ef..15afb22ab8 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,5 +1,8 @@ +from collections.abc import Callable, MutableMapping from enum import Enum +import logging import re +from typing import Any from esphome import automation import esphome.codegen as cg @@ -9,16 +12,19 @@ from esphome.const import ( CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, + CONF_MAX_CONNECTIONS, CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, ) -from esphome.core import TimePeriod +from esphome.core import CORE, TimePeriod import esphome.final_validate as fv DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" +_LOGGER = logging.getLogger(__name__) + class BTLoggers(Enum): """Bluetooth logger categories available in ESP-IDF. @@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs" CONF_CONNECTION_TIMEOUT = "connection_timeout" CONF_MAX_NOTIFICATIONS = "max_notifications" +# BLE connection limits +# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4 +# Total instances: 10 (ADV + SCAN + connections) +# - ADV only: up to 9 connections +# - SCAN only: up to 9 connections +# - ADV + SCAN: up to 8 connections +DEFAULT_MAX_CONNECTIONS = 3 +IDF_MAX_CONNECTIONS = 9 + +# Connection slot tracking keys +KEY_ESP32_BLE = "esp32_ble" +KEY_USED_CONNECTION_SLOTS = "used_connection_slots" + +# Export for use by other components (bluetooth_proxy, etc.) +__all__ = [ + "DEFAULT_MAX_CONNECTIONS", + "IDF_MAX_CONNECTIONS", + "KEY_ESP32_BLE", + "KEY_USED_CONNECTION_SLOTS", + "consume_connection_slots", +] + NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") @@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema( cv.positive_int, cv.Range(min=1, max=64), ), + cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( + cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS) + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -230,6 +261,56 @@ def validate_variant(_): raise cv.Invalid(f"{variant} does not support Bluetooth") +def consume_connection_slots( + value: int, consumer: str +) -> Callable[[MutableMapping], MutableMapping]: + """Reserve BLE connection slots for a component. + + Args: + value: Number of connection slots to reserve + consumer: Name of the component consuming the slots + + Returns: + A validator function that records the slot usage + """ + + def _consume_connection_slots(config: MutableMapping) -> MutableMapping: + data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {}) + slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) + slots.extend([consumer] * value) + return config + + return _consume_connection_slots + + +def validate_connection_slots(max_connections: int) -> None: + """Validate that BLE connection slots don't exceed the configured maximum.""" + ble_data = CORE.data.get(KEY_ESP32_BLE, {}) + used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, []) + num_used = len(used_slots) + + if num_used <= max_connections: + return + + slot_users = ", ".join(used_slots) + + if num_used > IDF_MAX_CONNECTIONS: + raise cv.Invalid( + f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. " + f"Reduce the number of BLE clients. Components: {slot_users}" + ) + + _LOGGER.warning( + "BLE components require %d connection slot(s) but only %d configured. " + "Please set 'max_connections: %d' in the 'esp32_ble' component. " + "Components: %s", + num_used, + max_connections, + num_used, + slot_users, + ) + + def final_validation(config): validate_variant(config) if (name := config.get(CONF_NAME)) is not None: @@ -245,6 +326,10 @@ def final_validation(config): # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() + # Validate connection slots usage + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + validate_connection_slots(max_connections) + # Check if BLE Server is needed has_ble_server = "esp32_ble_server" in full_config add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server) @@ -255,6 +340,26 @@ def final_validation(config): ) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) + # Handle max_connections: check for deprecated location in esp32_ble_tracker + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + + # Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat) + if "esp32_ble_tracker" in full_config: + tracker_config = full_config["esp32_ble_tracker"] + if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config: + max_connections = tracker_config["max_connections"] + + # Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN + # This is the Bluedroid host stack total instance limit (range 1-9, default 4) + # Total instances = ADV/SCAN (1) + connection slots (max_connections) + # Shared between client (tracker/ble_client) and server + add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1) + + # Set controller-specific max connections for ESP32 (classic) + # CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN) + # For newer chips (C3/S3/etc), different configs are used automatically + add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections) + return config @@ -270,6 +375,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) + # Define max connections for use in C++ code (e.g., ble_server.h) + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 64cef70de2..41a90150ef 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -68,6 +68,10 @@ void ESP32BLE::advertising_set_service_data(const std::vector &data) { } void ESP32BLE::advertising_set_manufacturer_data(const std::vector &data) { + this->advertising_set_manufacturer_data(std::span(data)); +} + +void ESP32BLE::advertising_set_manufacturer_data(std::span data) { this->advertising_init_(); this->advertising_->set_manufacturer_data(data); this->advertising_start(); @@ -213,15 +217,17 @@ bool ESP32BLE::ble_setup_() { if (this->name_.has_value()) { name = this->name_.value(); if (App.is_name_add_mac_suffix_enabled()) { - name += "-" + get_mac_address().substr(6); + name += "-"; + name += get_mac_address().substr(6); } } else { name = App.get_name(); if (name.length() > 20) { if (App.is_name_add_mac_suffix_enabled()) { - name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address + // Keep first 13 chars and last 7 chars (MAC suffix), remove middle + name.erase(13, name.length() - 20); } else { - name = name.substr(0, 20); + name.resize(20); } } } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 1aa3bc86ef..b49e5d12ee 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -118,6 +118,7 @@ class ESP32BLE : public Component { void advertising_start(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); + void advertising_set_manufacturer_data(std::span data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_service_data_and_name(std::span data, bool include_name); void advertising_add_service_uuid(ESPBTUUID uuid); diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index df70768c23..da105051eb 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -59,6 +59,10 @@ void BLEAdvertising::set_service_data(const std::vector &data) { } void BLEAdvertising::set_manufacturer_data(const std::vector &data) { + this->set_manufacturer_data(std::span(data)); +} + +void BLEAdvertising::set_manufacturer_data(std::span data) { delete[] this->advertising_data_.p_manufacturer_data; this->advertising_data_.p_manufacturer_data = nullptr; this->advertising_data_.manufacturer_len = data.size(); @@ -152,7 +156,7 @@ void BLEAdvertising::loop() { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { this->stop(); this->current_adv_index_ += 1; - if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { + if (static_cast(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) { this->current_adv_index_ = -1; } this->start(); diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 7a31d926f6..70d58d5ce9 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -35,6 +35,7 @@ class BLEAdvertising { void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_manufacturer_data(const std::vector &data); + void set_manufacturer_data(std::span data); void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_service_data(const std::vector &data); void set_service_data(std::span data); diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index 5f83e2ba0b..dcbb285e07 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -42,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ret; if (data.length() == 4) { - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = 0; - for (uint i = 0; i < data.length(); i += 2) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift; + // 16-bit UUID as 4-character hex string + auto parsed = parse_hex(data); + if (parsed.has_value()) { + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = parsed.value(); } } else if (data.length() == 8) { - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = 0; - for (uint i = 0; i < data.length(); i += 2) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift; + // 32-bit UUID as 8-character hex string + auto parsed = parse_hex(data); + if (parsed.has_value()) { + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = parsed.value(); } } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be // investigated (lack of time) @@ -145,28 +131,16 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { if (this->uuid_.len == uuid.uuid_.len) { switch (this->uuid_.len) { case ESP_UUID_LEN_16: - if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { - return true; - } - break; + return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16; case ESP_UUID_LEN_32: - if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { - return true; - } - break; + return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32; case ESP_UUID_LEN_128: - for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) { - if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { - return false; - } - } - return true; - break; + return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0; + default: + return false; } - } else { - return this->as_128bit() == uuid.as_128bit(); } - return false; + return this->as_128bit() == uuid.as_128bit(); } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } std::string ESPBTUUID::to_string() const { diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index d485d9fe2d..87f562a250 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -49,7 +49,11 @@ void BLECharacteristic::notify() { this->service_->get_server()->get_connected_client_count() == 0) return; - for (auto &client : this->service_->get_server()->get_clients()) { + const uint16_t *clients = this->service_->get_server()->get_clients(); + uint8_t client_count = this->service_->get_server()->get_client_count(); + + for (uint8_t i = 0; i < client_count; i++) { + uint16_t client = clients[i]; size_t length = this->value_.size(); // Find the client in the list of clients to notify auto *entry = this->find_client_in_notify_list_(client); @@ -121,69 +125,49 @@ bool BLECharacteristic::is_created() { if (this->state_ != CREATING_DEPENDENTS) return false; - bool created = true; for (auto *descriptor : this->descriptors_) { - created &= descriptor->is_created(); + if (!descriptor->is_created()) + return false; } - if (created) - this->state_ = CREATED; - return this->state_ == CREATED; + // All descriptors are created if we reach here + this->state_ = CREATED; + return true; } bool BLECharacteristic::is_failed() { if (this->state_ == FAILED) return true; - bool failed = false; for (auto *descriptor : this->descriptors_) { - failed |= descriptor->is_failed(); + if (descriptor->is_failed()) { + this->state_ = FAILED; + return true; + } + } + return false; +} + +void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) { + if (value) { + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | bit); + } else { + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~bit); } - if (failed) - this->state_ = FAILED; - return this->state_ == FAILED; } void BLECharacteristic::set_broadcast_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value); } void BLECharacteristic::set_indicate_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value); } void BLECharacteristic::set_notify_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); - } -} -void BLECharacteristic::set_read_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); - } -} -void BLECharacteristic::set_write_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value); } +void BLECharacteristic::set_read_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_READ, value); } +void BLECharacteristic::set_write_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE, value); } void BLECharacteristic::set_write_no_response_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value); } void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 4a29683f41..7cceec0ef1 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -97,6 +97,8 @@ class BLECharacteristic { void remove_client_from_notify_list_(uint16_t conn_id); ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id); + void set_property_bit_(esp_gatt_char_prop_t bit, bool value); + std::unique_ptr, uint16_t)>> on_write_callback_; std::unique_ptr> on_read_callback_; diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 942be7e597..25cc97eeaf 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -185,9 +185,38 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga } } +int8_t BLEServer::find_client_index_(uint16_t conn_id) const { + for (uint8_t i = 0; i < this->client_count_; i++) { + if (this->clients_[i] == conn_id) + return i; + } + return -1; +} + +void BLEServer::add_client_(uint16_t conn_id) { + // Check if already in list + if (this->find_client_index_(conn_id) >= 0) + return; + // Add if there's space + if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) { + this->clients_[this->client_count_++] = conn_id; + } else { + // This should never happen since max clients is known at compile time + ESP_LOGE(TAG, "Client array full"); + } +} + +void BLEServer::remove_client_(uint16_t conn_id) { + int8_t index = this->find_client_index_(conn_id); + if (index >= 0) { + // Replace with last element and decrement count (client order not preserved) + this->clients_[index] = this->clients_[--this->client_count_]; + } +} + void BLEServer::ble_before_disabled_event_handler() { // Delete all clients - this->clients_.clear(); + this->client_count_ = 0; // Delete all services for (auto &entry : this->services_) { entry.service->do_delete(); diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 48005b1346..6fa86dd67f 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #ifdef USE_ESP32 @@ -47,8 +46,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void set_device_information_service(BLEService *service) { this->device_information_service_ = service; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } - uint32_t get_connected_client_count() { return this->clients_.size(); } - const std::unordered_set &get_clients() { return this->clients_; } + uint32_t get_connected_client_count() { return this->client_count_; } + const uint16_t *get_clients() const { return this->clients_; } + uint8_t get_client_count() const { return this->client_count_; } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) override; @@ -82,8 +82,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void restart_advertising_(); - void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } - void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } + int8_t find_client_index_(uint16_t conn_id) const; + void add_client_(uint16_t conn_id); + void remove_client_(uint16_t conn_id); void dispatch_callbacks_(CallbackType type, uint16_t conn_id); std::vector callbacks_; @@ -92,7 +93,8 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv esp_gatt_if_t gatts_if_{0}; bool registered_{false}; - std::unordered_set clients_; + uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; + uint8_t client_count_{0}; std::vector services_{}; std::vector services_to_start_{}; BLEService *device_information_service_{}; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 787fb9fb65..247496ccd9 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,14 +1,13 @@ from __future__ import annotations -from collections.abc import Callable, MutableMapping import logging -from typing import Any from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import ( + IDF_MAX_CONNECTIONS, BTLoggers, bt_uuid, bt_uuid16_format, @@ -24,6 +23,7 @@ from esphome.const import ( CONF_INTERVAL, CONF_MAC_ADDRESS, CONF_MANUFACTURER_ID, + CONF_MAX_CONNECTIONS, CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE, @@ -38,19 +38,12 @@ AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] CODEOWNERS = ["@bdraco"] -KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" -KEY_USED_CONNECTION_SLOTS = "used_connection_slots" - -CONF_MAX_CONNECTIONS = "max_connections" CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" CONF_ON_SCAN_END = "on_scan_end" CONF_SOFTWARE_COEXISTENCE = "software_coexistence" -DEFAULT_MAX_CONNECTIONS = 3 -IDF_MAX_CONNECTIONS = 9 - _LOGGER = logging.getLogger(__name__) @@ -128,6 +121,15 @@ def validate_scan_parameters(config): return config +def validate_max_connections_deprecated(config: ConfigType) -> ConfigType: + if CONF_MAX_CONNECTIONS in config: + _LOGGER.warning( + "The 'max_connections' option in 'esp32_ble_tracker' is deprecated. " + "Please move it to the 'esp32_ble' component instead." + ) + return config + + def as_hex(value): return cg.RawExpression(f"0x{value}ULL") @@ -150,24 +152,12 @@ def as_reversed_hex_array(value): ) -def consume_connection_slots( - value: int, consumer: str -) -> Callable[[MutableMapping], MutableMapping]: - def _consume_connection_slots(config: MutableMapping) -> MutableMapping: - data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {}) - slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) - slots.extend([consumer] * value) - return config - - return _consume_connection_slots - - CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( + cv.Optional(CONF_MAX_CONNECTIONS): cv.All( cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS) ), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( @@ -224,48 +214,11 @@ CONFIG_SCHEMA = cv.All( cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool, } ).extend(cv.COMPONENT_SCHEMA), + validate_max_connections_deprecated, ) -def validate_remaining_connections(config): - data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {}) - slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, []) - used_slots = len(slots) - if used_slots <= config[CONF_MAX_CONNECTIONS]: - return config - slot_users = ", ".join(slots) - - if used_slots < IDF_MAX_CONNECTIONS: - _LOGGER.warning( - "esp32_ble_tracker exceeded `%s`: components attempted to consume %d " - "connection slot(s) out of available configured maximum %d connection " - "slot(s); The system automatically increased `%s` to %d to match the " - "number of used connection slot(s) by components: %s.", - CONF_MAX_CONNECTIONS, - used_slots, - config[CONF_MAX_CONNECTIONS], - CONF_MAX_CONNECTIONS, - used_slots, - slot_users, - ) - config[CONF_MAX_CONNECTIONS] = used_slots - return config - - msg = ( - f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: " - f"components attempted to consume {used_slots} connection slot(s) " - f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} " - f"connection slot(s); Decrease the number of BLE clients ({slot_users})" - ) - if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS: - msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}" - msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit." - raise cv.Invalid(msg) - - -FINAL_VALIDATE_SCHEMA = cv.All( - validate_remaining_connections, esp32_ble.validate_variant -) +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant ESP_BLE_DEVICE_SCHEMA = cv.Schema( { @@ -345,10 +298,8 @@ async def to_code(config): # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) - add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) - add_idf_sdkconfig_option( - "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] - ) + # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now + # configured in esp32_ble component based on max_connections setting cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index b5e72497ce..cdef7b1930 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config } bool ESP32Can::setup_internal() { + static int next_twai_ctrl_num = 0; + if (static_cast(next_twai_ctrl_num) >= SOC_TWAI_CONTROLLER_NUM) { + ESP_LOGW(TAG, "Maximum number of esp32_can components created already"); + this->mark_failed(); + return false; + } + twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + g_config.controller_id = next_twai_ctrl_num++; if (this->tx_queue_len_.has_value()) { g_config.tx_queue_len = this->tx_queue_len_.value(); } @@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() { } // Install TWAI driver - if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) { // Failed to install driver this->mark_failed(); return false; } // Start TWAI driver - if (twai_start() != ESP_OK) { + if (twai_start_v2(this->twai_handle_) != ESP_OK) { // Failed to start driver this->mark_failed(); return false; @@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() { } canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { + if (this->twai_handle_ == nullptr) { + // not setup yet or setup failed + return canbus::ERROR_FAIL; + } + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { return canbus::ERROR_FAILTX; } @@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { memcpy(message.data, frame->data, frame->can_data_length_code); } - if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { + if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { return canbus::ERROR_OK; } else { return canbus::ERROR_ALLTXBUSY; @@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { } canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { + if (this->twai_handle_ == nullptr) { + // not setup yet or setup failed + return canbus::ERROR_FAIL; + } + twai_message_t message; - if (twai_receive(&message, 0) != ESP_OK) { + if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) { return canbus::ERROR_NOMSG; } diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index 416f037083..dc44aceb36 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -5,6 +5,8 @@ #include "esphome/components/canbus/canbus.h" #include "esphome/core/component.h" +#include + namespace esphome { namespace esp32_can { @@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus { TickType_t tx_enqueue_timeout_ticks_{}; optional tx_queue_len_{}; optional rx_queue_len_{}; + twai_handle_t twai_handle_{nullptr}; }; } // namespace esp32_can diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 344ea35e81..fa43aa5950 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -35,7 +35,7 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size if (symbols_free < RMT_SYMBOLS_PER_BYTE) { return 0; } - for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { + for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { if (bytes[index] & (1 << (7 - i))) { symbols[i] = params->bit1; } else { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f1506f066c..b65bfc5ab8 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } - // Generate nonce with appropriate hasher - bool success = false; + // Generate nonce - hasher must be created and used in same stack frame + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: + // 1. Hash objects must NEVER be passed to another function (different stack frame) + // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA + // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created + // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption. + // + // Buffer layout after AUTH_READ completes: + // [0]: auth_type (1 byte) + // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND + // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce + // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash + + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; + #ifdef USE_OTA_SHA256 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - sha256::SHA256 sha_hasher; - success = this->prepare_auth_nonce_(&sha_hasher); + hasher = &sha_hasher; } #endif #ifdef USE_OTA_MD5 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - md5::MD5Digest md5_hasher; - success = this->prepare_auth_nonce_(&md5_hasher); + hasher = &md5_hasher; } #endif - if (!success) { + const size_t hex_size = hasher->get_size() * 2; + const size_t nonce_len = hasher->get_size() / 4; + const size_t auth_buf_size = 1 + 3 * hex_size; + this->auth_buf_ = std::make_unique(auth_buf_size); + this->auth_buf_pos_ = 0; + + char *buf = reinterpret_cast(this->auth_buf_.get() + 1); + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { + this->log_auth_warning_(LOG_STR("Random failed")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); return false; } + + hasher->init(); + hasher->add(buf, nonce_len); + hasher->calculate(); + this->auth_buf_[0] = this->auth_type_; + hasher->get_hex(buf); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too + memcpy(log_buf, buf, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); +#endif } // Try to write auth_type + nonce @@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() { } // We have all the data, verify it - bool matches = false; + const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); + const char *cnonce = nonce + hex_size; + const char *response = cnonce + hex_size; + + // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions). + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; #ifdef USE_OTA_SHA256 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - sha256::SHA256 sha_hasher; - matches = this->verify_hash_auth_(&sha_hasher, hex_size); + hasher = &sha_hasher; } #endif #ifdef USE_OTA_MD5 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - md5::MD5Digest md5_hasher; - matches = this->verify_hash_auth_(&md5_hasher, hex_size); + hasher = &md5_hasher; } #endif - if (!matches) { - this->log_auth_warning_(LOG_STR("Password mismatch")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; - } - - // Authentication successful - clean up auth state - this->cleanup_auth_(); - - return true; -} - -bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { - // Calculate required buffer size using the hasher - const size_t hex_size = hasher->get_size() * 2; - const size_t nonce_len = hasher->get_size() / 4; - - // Buffer layout after AUTH_READ completes: - // [0]: auth_type (1 byte) - // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND - // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce - // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - // Total: 1 + 3*hex_size - const size_t auth_buf_size = 1 + 3 * hex_size; - this->auth_buf_ = std::make_unique(auth_buf_size); - this->auth_buf_pos_ = 0; - - // Generate nonce - char *buf = reinterpret_cast(this->auth_buf_.get() + 1); - if (!random_bytes(reinterpret_cast(buf), nonce_len)) { - this->log_auth_warning_(LOG_STR("Random failed")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); - return false; - } - - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); - - // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) - this->auth_buf_[0] = this->auth_type_; - hasher->get_hex(buf); - -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; - // Log nonce for debugging - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); -#endif - - return true; -} - -bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { - // Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout) - const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte - const char *cnonce = nonce + hex_size; // CNonce immediately follows nonce - const char *response = cnonce + hex_size; // Response immediately follows cnonce - - // Calculate expected hash: password + nonce + cnonce hasher->init(); hasher->add(this->password_.c_str(), this->password_.length()); hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too // Log CNonce memcpy(log_buf, cnonce, hex_size); log_buf[hex_size] = '\0'; @@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { #endif // Compare response - return hasher->equals_hex(response); + bool matches = hasher->equals_hex(response); + + if (!matches) { + this->log_auth_warning_(LOG_STR("Password mismatch")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + + // Authentication successful - clean up auth state + this->cleanup_auth_(); + + return true; } size_t ESPHomeOTAComponent::get_auth_hex_size_() const { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 1e26494fd0..d4a8410d35 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool handle_auth_send_(); bool handle_auth_read_(); bool select_auth_type_(); - bool prepare_auth_nonce_(HashBase *hasher); - bool verify_hash_auth_(HashBase *hasher, size_t hex_size); size_t get_auth_hex_size_() const; void cleanup_auth_(); void log_auth_warning_(const LogString *msg); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 16f5903e3f..28043dd969 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -41,17 +41,20 @@ static const char *const TAG = "ethernet"; EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { + ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); + this->mark_failed(); +} + #define ESPHL_ERROR_CHECK(err, message) \ if ((err) != ESP_OK) { \ - ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ - this->mark_failed(); \ + this->log_error_and_mark_failed_(err, message); \ return; \ } #define ESPHL_ERROR_CHECK_RET(err, message, ret) \ if ((err) != ESP_OK) { \ - ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ - this->mark_failed(); \ + this->log_error_and_mark_failed_(err, message); \ return ret; \ } diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 9a0da12241..6b4e342df5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -106,6 +106,7 @@ class EthernetComponent : public Component { void start_connect_(); void finish_connect_(); void dump_connect_params_(); + void log_error_and_mark_failed_(esp_err_t err, const char *message); #ifdef USE_ETHERNET_KSZ8081 /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); @@ -162,7 +163,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 54a267a404..eb7ede8fe9 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() { delay(20); // This delay guarantees the sensor will in fact be powered power. if (this->check_password_()) { - if (this->new_password_ != -1) { + if (this->new_password_ != std::numeric_limits::max()) { if (this->set_password_()) return; } else { diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 1c3098ef14..590c709c22 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -6,6 +6,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include #include namespace esphome { @@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint16_t capacity_ = 64; uint32_t password_ = 0x0; - uint32_t new_password_ = -1; + uint32_t new_password_ = std::numeric_limits::max(); GPIOPin *sensing_pin_{nullptr}; GPIOPin *sensor_power_pin_{nullptr}; uint8_t enrollment_image_ = 0; diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 5abf2ade0d..ac6ace96ee 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo if (b) { 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) { - if (y >= y_offset && y < y_offset + this->height_) + if (y >= y_offset && static_cast(y) < y_offset + this->height_) buff->draw_pixel_at(x, y, c); }; if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 1a29536b46..2b120a746f 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const int number_items_fit_to_screen = 0; const int max_item_index = this->displayed_item_->items_size() - 1; - for (size_t i = 0; i <= max_item_index; i++) { + for (size_t i = 0; max_item_index >= 0 && i <= static_cast(max_item_index); i++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); @@ -174,7 +174,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_); auto y_offset = bounds->y; - for (size_t i = first_item_index; i <= last_item_index; i++) { + for (size_t i = static_cast(first_item_index); + last_item_index >= 0 && i <= static_cast(last_item_index); i++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; display::Rect dimensions = menu_dimensions[i]; diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 9614bb1e47..76558f2ebb 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy this->real_control_packet_size_); this->status_message_callback_.call((const char *) data, data_size); } else { - ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_); + ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_); } switch (this->protocol_phase_) { case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: @@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * size_t expected_size = 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; if (size < expected_size) { - ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size); + ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size); return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; } uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 58173f8154..a567ab1d89 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase { int extra_control_packet_bytes_{0}; int extra_sensors_packet_bytes_{4}; int status_message_header_size_{0}; - int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; + size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4}; HonControlMethod control_method_; std::queue control_messages_queue_; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 98dbc29a86..e428838c83 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -5,6 +5,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_CAPTURE_RESPONSE, CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ID, CONF_METHOD, @@ -57,7 +58,6 @@ CONF_HEADERS = "headers" CONF_COLLECT_HEADERS = "collect_headers" CONF_BODY = "body" CONF_JSON = "json" -CONF_CAPTURE_RESPONSE = "capture_response" def validate_url(value): diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 7ae3ec8b3b..53e378c41e 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { size_t len = bytes_read / sizeof(int16_t); int16_t *tmp_buf = (int16_t *) new_data; - for (int i = 0; i < len; i += 2) { + for (size_t i = 0; i < len; i += 2) { int16_t tmp = tmp_buf[i]; tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i + 1] = tmp; diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index ec0a860aa8..2a3d0edca7 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -325,7 +325,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother this->write_array(ptr, w * h * 2); } else { - for (size_t y = 0; y != h; y++) { + for (size_t y = 0; y != static_cast(h); y++) { this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); } } @@ -349,7 +349,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons App.feed_wdt(); } // end of line? Skip to the next. - if (++pixel == w) { + if (++pixel == static_cast(w)) { pixel = 0; ptr += (x_pad + x_offset) * 2; } diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 643f23f499..dbdf6e3486 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -19,15 +19,19 @@ std::string build_json(const json_build_t &f) { bool parse_json(const std::string &data, const json_parse_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - JsonDocument doc = parse_json(data); + JsonDocument doc = parse_json(reinterpret_cast(data.c_str()), data.size()); if (doc.overflowed() || doc.isNull()) return false; return f(doc.as()); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -JsonDocument parse_json(const std::string &data) { +JsonDocument parse_json(const uint8_t *data, size_t len) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (data == nullptr || len == 0) { + ESP_LOGE(TAG, "No data to parse"); + return JsonObject(); // return unbound object + } #ifdef USE_PSRAM auto doc_allocator = SpiRamAllocator(); JsonDocument json_document(&doc_allocator); @@ -38,7 +42,7 @@ JsonDocument parse_json(const std::string &data) { ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); return JsonObject(); // return unbound object } - DeserializationError err = deserializeJson(json_document, data); + DeserializationError err = deserializeJson(json_document, data, len); if (err == DeserializationError::Ok) { return json_document; diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 0349833342..91cc84dc14 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -2,6 +2,7 @@ #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT @@ -49,8 +50,13 @@ std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); + /// Parse a JSON string and return the root JsonDocument (or an unbound object on error) -JsonDocument parse_json(const std::string &data); +JsonDocument parse_json(const uint8_t *data, size_t len); +/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) +inline JsonDocument parse_json(const std::string &data) { + return parse_json(reinterpret_cast(data.c_str()), data.size()); +} /// Builder class for creating JSON documents without lambdas class JsonBuilder { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index c058c7b3aa..e5fa035682 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() { LOG_SENSOR(" ", "Flow", this->flow_sensor_); LOG_SENSOR(" ", "Volume", this->volume_sensor_); - for (int i = 0; i < this->custom_sensors_.size(); i++) { + for (size_t i = 0; i < this->custom_sensors_.size(); i++) { LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); } @@ -268,7 +268,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint } // Custom sensors - for (int i = 0; i < this->custom_commands_.size(); i++) { + for (size_t i = 0; i < this->custom_commands_.size(); i++) { if (command == this->custom_commands_[i]) { this->custom_sensors_[i]->publish_state(value); } diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 6e585ddd8e..35e8141ce5 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -13,8 +13,8 @@ class KeyCollector : public Component { void loop() override; void dump_config() override; void set_provider(key_provider::KeyProvider *provider); - void set_min_length(int min_length) { this->min_length_ = min_length; }; - void set_max_length(int max_length) { this->max_length_ = max_length; }; + void set_min_length(uint32_t min_length) { this->min_length_ = min_length; }; + void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; @@ -33,8 +33,8 @@ class KeyCollector : public Component { protected: void key_pressed_(uint8_t key); - int min_length_{0}; - int max_length_{0}; + uint32_t min_length_{0}; + uint32_t max_length_{0}; std::string start_keys_; std::string end_keys_; bool end_key_required_{false}; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index cbe9ed0454..915b8fdf89 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -10,11 +10,15 @@ namespace light { static const char *const TAG = "light"; // Helper functions to reduce code size for logging -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN -static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) { - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max); +static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f, + float max = 1.0f) { + if (value < min || value > max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max); + value = clamp(value, min, max); + } } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN static void log_feature_not_supported(const char *name, const LogString *feature) { ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature)); } @@ -27,7 +31,6 @@ static void log_invalid_parameter(const char *name, const LogString *message) { ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message)); } #else -#define log_validation_warning(name, param_name, val, min, max) #define log_feature_not_supported(name, feature) #define log_color_mode_not_supported(name, feature) #define log_invalid_parameter(name, message) @@ -44,7 +47,7 @@ static void log_invalid_parameter(const char *name, const LogString *message) { } \ LightCall &LightCall::set_##name(type name) { \ this->name##_ = name; \ - this->set_flag_(flag, true); \ + this->set_flag_(flag); \ return *this; \ } @@ -181,6 +184,16 @@ void LightCall::perform() { } } +void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) { + auto *name = this->parent_->get_name().c_str(); + if (use_color_mode_log) { + log_color_mode_not_supported(name, feature); + } else { + log_feature_not_supported(name, feature); + } + this->clear_flag_(flag); +} + LightColorValues LightCall::validate_() { auto *name = this->parent_->get_name().c_str(); auto traits = this->parent_->get_traits(); @@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() { // Color mode check if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); - this->set_flag_(FLAG_HAS_COLOR_MODE, false); + this->clear_flag_(FLAG_HAS_COLOR_MODE); } // Ensure there is always a color mode set if (!this->has_color_mode()) { this->color_mode_ = this->compute_color_mode_(); - this->set_flag_(FLAG_HAS_COLOR_MODE, true); + this->set_flag_(FLAG_HAS_COLOR_MODE); } auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. this->transform_parameters_(); - // Brightness exists check - if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, LOG_STR("brightness")); - this->set_flag_(FLAG_HAS_BRIGHTNESS, false); - } - - // Transition length possible check - if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, LOG_STR("transitions")); - this->set_flag_(FLAG_HAS_TRANSITION, false); - } - - // Color brightness exists check - if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, LOG_STR("RGB brightness")); - this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); - } - - // RGB exists check - if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || - (this->has_blue() && this->blue_ > 0.0f)) { - if (!(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, LOG_STR("RGB color")); - this->set_flag_(FLAG_HAS_RED, false); - this->set_flag_(FLAG_HAS_GREEN, false); - this->set_flag_(FLAG_HAS_BLUE, false); - } - } - - // White value exists check - if (this->has_white() && this->white_ > 0.0f && - !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, LOG_STR("white value")); - this->set_flag_(FLAG_HAS_WHITE, false); - } - - // Color temperature exists check - if (this->has_color_temperature() && - !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, LOG_STR("color temperature")); - this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); - } - - // Cold/warm white value exists check - if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { - if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); - this->set_flag_(FLAG_HAS_COLD_WHITE, false); - this->set_flag_(FLAG_HAS_WARM_WHITE, false); - } - } - -#define VALIDATE_RANGE_(name_, upper_name, min, max) \ - if (this->has_##name_()) { \ - auto val = this->name_##_; \ - if (val < (min) || val > (max)) { \ - log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \ - this->name_##_ = clamp(val, (min), (max)); \ - } \ - } -#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) - - // Range checks - VALIDATE_RANGE(brightness, "Brightness") - VALIDATE_RANGE(color_brightness, "Color brightness") - VALIDATE_RANGE(red, "Red") - VALIDATE_RANGE(green, "Green") - VALIDATE_RANGE(blue, "Blue") - VALIDATE_RANGE(white, "White") - VALIDATE_RANGE(cold_white, "Cold white") - VALIDATE_RANGE(warm_white, "Warm white") - VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) - + // Business logic adjustments before validation // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. bool explicit_turn_off_request = this->has_state() && !this->state_; // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). if (this->has_brightness() && this->brightness_ == 0.0f) { this->state_ = false; - this->set_flag_(FLAG_HAS_STATE, true); + this->set_flag_(FLAG_HAS_STATE); this->brightness_ = 1.0f; } // Set color brightness to 100% if currently zero and a color is set. - if (this->has_red() || this->has_green() || this->has_blue()) { - if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { - this->color_brightness_ = 1.0f; - this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); - } + if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() && + this->parent_->remote_values.get_color_brightness() == 0.0f) { + this->color_brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS); } - // Create color values for the light with this call applied. + // Capability validation + if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) + this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false); + + // Transition length possible check + if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) + this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); + + if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) + this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true); + + // RGB exists check + if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || + (this->has_blue() && this->blue_ > 0.0f)) && + !(color_mode & ColorCapability::RGB)) { + log_color_mode_not_supported(name, LOG_STR("RGB color")); + this->clear_flag_(FLAG_HAS_RED); + this->clear_flag_(FLAG_HAS_GREEN); + this->clear_flag_(FLAG_HAS_BLUE); + } + + // White value exists check + if (this->has_white() && this->white_ > 0.0f && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) + this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true); + + // Color temperature exists check + if (this->has_color_temperature() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) + this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true); + + // Cold/warm white value exists check + if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) && + !(color_mode & ColorCapability::COLD_WARM_WHITE)) { + log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); + this->clear_flag_(FLAG_HAS_COLD_WHITE); + this->clear_flag_(FLAG_HAS_WARM_WHITE); + } + + // Create color values and validate+apply ranges in one step to eliminate duplicate checks auto v = this->parent_->remote_values; if (this->has_color_mode()) v.set_color_mode(this->color_mode_); if (this->has_state()) v.set_state(this->state_); - if (this->has_brightness()) - v.set_brightness(this->brightness_); - if (this->has_color_brightness()) - v.set_color_brightness(this->color_brightness_); - if (this->has_red()) - v.set_red(this->red_); - if (this->has_green()) - v.set_green(this->green_); - if (this->has_blue()) - v.set_blue(this->blue_); - if (this->has_white()) - v.set_white(this->white_); - if (this->has_color_temperature()) - v.set_color_temperature(this->color_temperature_); - if (this->has_cold_white()) - v.set_cold_white(this->cold_white_); - if (this->has_warm_white()) - v.set_warm_white(this->warm_white_); + +#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ + if (this->has_##field()) { \ + clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ + v.setter(this->field##_); \ + } + + VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") + VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") + VALIDATE_AND_APPLY(red, set_red, "Red") + VALIDATE_AND_APPLY(green, set_green, "Green") + VALIDATE_AND_APPLY(blue, set_blue, "Blue") + VALIDATE_AND_APPLY(white, set_white, "White") + VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") + VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") + VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), + traits.get_max_mireds()) + +#undef VALIDATE_AND_APPLY v.normalize_color(); // Flash length check if (this->has_flash_() && this->flash_length_ == 0) { - log_invalid_parameter(name, LOG_STR("flash length must be greater than zero")); - this->set_flag_(FLAG_HAS_FLASH, false); + log_invalid_parameter(name, LOG_STR("flash length must be >0")); + this->clear_flag_(FLAG_HAS_FLASH); } // validate transition length/flash length/effect not used at the same time @@ -330,42 +310,40 @@ LightColorValues LightCall::validate_() { // If effect is already active, remove effect start if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } // validate effect index if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); - this->set_flag_(FLAG_HAS_TRANSITION, false); - this->set_flag_(FLAG_HAS_FLASH, false); + this->clear_flag_(FLAG_HAS_TRANSITION); + this->clear_flag_(FLAG_HAS_FLASH); } if (this->has_flash_() && this->has_transition_()) { log_invalid_parameter(name, LOG_STR("flash cannot be used with transition")); - this->set_flag_(FLAG_HAS_TRANSITION, false); + this->clear_flag_(FLAG_HAS_TRANSITION); } if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) && supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; - this->set_flag_(FLAG_HAS_TRANSITION, true); + this->set_flag_(FLAG_HAS_TRANSITION); } if (this->has_transition_() && this->transition_length_ == 0) { // 0 transition is interpreted as no transition (instant change) - this->set_flag_(FLAG_HAS_TRANSITION, false); + this->clear_flag_(FLAG_HAS_TRANSITION); } - if (this->has_transition_() && !supports_transition) { - log_feature_not_supported(name, LOG_STR("transitions")); - this->set_flag_(FLAG_HAS_TRANSITION, false); - } + if (this->has_transition_() && !supports_transition) + this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); // If not a flash and turning the light off, then disable the light // Do not use light color values directly, so that effects can set 0% brightness @@ -374,17 +352,17 @@ LightColorValues LightCall::validate_() { if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect this->effect_ = 0; - this->set_flag_(FLAG_HAS_EFFECT, true); + this->set_flag_(FLAG_HAS_EFFECT); } } // Disable saving for flashes if (this->has_flash_()) - this->set_flag_(FLAG_SAVE, false); + this->clear_flag_(FLAG_SAVE); return v; } @@ -418,12 +396,12 @@ void LightCall::transform_parameters_() { const float gamma = this->parent_->get_gamma_correct(); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); - this->set_flag_(FLAG_HAS_COLD_WHITE, true); - this->set_flag_(FLAG_HAS_WARM_WHITE, true); + this->set_flag_(FLAG_HAS_COLD_WHITE); + this->set_flag_(FLAG_HAS_WARM_WHITE); } if (this->has_white()) { this->brightness_ = this->white_; - this->set_flag_(FLAG_HAS_BRIGHTNESS, true); + this->set_flag_(FLAG_HAS_BRIGHTNESS); } } } @@ -630,7 +608,7 @@ LightCall &LightCall::set_effect(optional effect) { } LightCall &LightCall::set_effect(uint32_t effect_number) { this->effect_ = effect_number; - this->set_flag_(FLAG_HAS_EFFECT, true); + this->set_flag_(FLAG_HAS_EFFECT); return *this; } LightCall &LightCall::set_effect(optional effect_number) { diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 7e04e1a767..d3a526b136 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -4,6 +4,10 @@ #include namespace esphome { + +// Forward declaration +struct LogString; + namespace light { class LightState; @@ -207,14 +211,14 @@ class LightCall { FLAG_SAVE = 1 << 15, }; - bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } - bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } - bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } - bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } - bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } + inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } + inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } + inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } + inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } + inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } - // Helper to set flag - void set_flag_(FieldFlags flag, bool value) { + // Helper to set flag - defaults to true for common case + void set_flag_(FieldFlags flag, bool value = true) { if (value) { this->flags_ |= flag; } else { @@ -222,6 +226,12 @@ class LightCall { } } + // Helper to clear flag - reduces code size for common case + void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; } + + // Helper to log unsupported feature and clear flag - reduces code duplication + void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log); + LightState *parent_; // Light state values - use flags_ to check if a value has been set. diff --git a/esphome/components/lm75b/__init__.py b/esphome/components/lm75b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/lm75b/lm75b.cpp b/esphome/components/lm75b/lm75b.cpp new file mode 100644 index 0000000000..19398eda85 --- /dev/null +++ b/esphome/components/lm75b/lm75b.cpp @@ -0,0 +1,39 @@ +#include "lm75b.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace lm75b { + +static const char *const TAG = "lm75b"; + +void LM75BComponent::dump_config() { + ESP_LOGCONFIG(TAG, "LM75B:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up LM75B failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} + +void LM75BComponent::update() { + // Create a temporary buffer + uint8_t buff[2]; + if (this->read_register(LM75B_REG_TEMPERATURE, buff, 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + // Obtain combined 16-bit value + int16_t raw_temperature = (buff[0] << 8) | buff[1]; + // Read the 11-bit raw temperature value + raw_temperature >>= 5; + // Publish the temperature in °C + this->publish_state(raw_temperature * 0.125); + if (this->status_has_warning()) { + this->status_clear_warning(); + } +} + +} // namespace lm75b +} // namespace esphome diff --git a/esphome/components/lm75b/lm75b.h b/esphome/components/lm75b/lm75b.h new file mode 100644 index 0000000000..79d9fa3f32 --- /dev/null +++ b/esphome/components/lm75b/lm75b.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace lm75b { + +static const uint8_t LM75B_REG_TEMPERATURE = 0x00; + +class LM75BComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void dump_config() override; + void update() override; +}; + +} // namespace lm75b +} // namespace esphome diff --git a/esphome/components/lm75b/sensor.py b/esphome/components/lm75b/sensor.py new file mode 100644 index 0000000000..335446b62f --- /dev/null +++ b/esphome/components/lm75b/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@beormund"] +DEPENDENCIES = ["i2c"] + +lm75b_ns = cg.esphome_ns.namespace("lm75b") +LM75BComponent = lm75b_ns.class_( + "LM75BComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + LM75BComponent, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 7d1a591f0c..1d02073d27 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -95,6 +95,7 @@ DEFAULT = "DEFAULT" CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" +CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" UART_SELECTION_ESP32 = { @@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_INITIAL_LEVEL): is_log_level, + cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), @@ -291,8 +293,12 @@ async def to_code(config): ) cg.add(log.pre_setup()) - for tag, log_level in config[CONF_LOGS].items(): - cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) + # Enable runtime tag levels if logs are configured or explicitly enabled + logs_config = config[CONF_LOGS] + if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") + for tag, log_level in logs_config.items(): + cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) cg.add_define("USE_LOGGER") this_severity = LOG_LEVEL_SEVERITY.index(level) @@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): level = LOG_LEVELS[config[CONF_LEVEL]] logger = await cg.get_variable(config[CONF_LOGGER_ID]) if tag := config.get(CONF_TAG): + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") text = str(cg.statement(logger.set_log_level(tag, level))) else: text = str(cg.statement(logger.set_log_level(level))) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 4a69bd9853..9a9bf89fe3 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas #endif // USE_STORE_LOG_STR_IN_FLASH inline uint8_t Logger::level_for(const char *tag) { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; +#endif return this->current_level_; } @@ -220,7 +222,9 @@ void Logger::process_messages_() { } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } @@ -271,9 +275,11 @@ void Logger::dump_config() { } #endif +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); } +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index a1f3df97dd..2099520049 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,6 +36,13 @@ struct device; namespace esphome::logger { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +// Comparison function for const char* keys in log_levels_ map +struct CStrCompare { + bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; } +}; +#endif + // ANSI color code last digit (30-38 range, store only last digit to save RAM) static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { '\0', // NONE @@ -133,8 +140,10 @@ class Logger : public Component { /// Set the default log level for this logger. void set_log_level(uint8_t level); +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, uint8_t log_level); + void set_log_level(const char *tag, uint8_t log_level); +#endif uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== @@ -242,7 +251,9 @@ class Logger : public Component { #endif // Large objects (internally aligned) - std::map log_levels_{}; +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS + std::map log_levels_{}; +#endif CallbackManager log_callback_{}; CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER @@ -355,6 +366,12 @@ class Logger : public Component { buffer[pos++] = '['; copy_string(buffer, pos, tag); buffer[pos++] = ':'; + // Format line number without modulo operations (passed by value, safe to mutate) + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + buffer[pos++] = '0' + thousands; + line -= thousands * 1000; + } int hundreds = line / 100; int remainder = line - hundreds * 100; int tens = remainder / 10; diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index d9c950ce3c..6d60a3ae47 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -3,11 +3,10 @@ namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { - auto value = this->at(level); - if (!value) { + const auto &option = this->at(level_to_index(level)); + if (!option) return; - } - Select::publish_state(value.value()); + Select::publish_state(option.value()); } void LoggerLevelSelect::setup() { @@ -16,10 +15,10 @@ void LoggerLevelSelect::setup() { } void LoggerLevelSelect::control(const std::string &value) { - auto level = this->index_of(value); - if (!level) + const auto index = this->index_of(value); + if (!index) return; - this->parent_->set_log_level(level.value()); + this->parent_->set_log_level(index_to_level(index.value())); } } // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index f31a6f6cdb..0631eca45d 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -3,11 +3,18 @@ #include "esphome/components/select/select.h" #include "esphome/core/component.h" #include "esphome/components/logger/logger.h" + namespace esphome::logger { class LoggerLevelSelect : public Component, public select::Select, public Parented { public: void publish_state(int level); void setup() override; void control(const std::string &value) override; + + protected: + // Convert log level to option index (skip CONFIG at level 4) + static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } + // Convert option index to log level (skip CONFIG at level 4) + static uint8_t index_to_level(uint8_t index) { return (index >= ESPHOME_LOG_LEVEL_CONFIG) ? index + 1 : index; } }; } // namespace esphome::logger diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index b249d23666..be5a4ddccf 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include using esphome::i2c::ErrorCode; @@ -28,30 +29,30 @@ bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) { template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index bf27c01e26..c3ea5848c8 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include using esphome::i2c::ErrorCode; @@ -14,30 +15,30 @@ static const uint8_t MAX_TRIES = 5; template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 8b309b42c2..258ab4fadc 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -29,9 +29,9 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { void set_columns(std::vector pins) { columns_ = std::move(pins); }; void set_rows(std::vector pins) { rows_ = std::move(pins); }; void set_keys(std::string keys) { keys_ = std::move(keys); }; - void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; - void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; - void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; + void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; }; + void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; }; + void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void register_listener(MatrixKeypadListener *listener); void register_key_trigger(MatrixKeyTrigger *trig); @@ -40,7 +40,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { std::vector rows_; std::vector columns_; std::string keys_; - int debounce_time_ = 0; + uint32_t debounce_time_ = 0; bool has_diodes_{false}; bool has_pulldowns_{false}; int pressed_key_ = -1; diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 9b9921d2f0..6df3c4d7c8 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -90,7 +90,7 @@ void MAX7219Component::loop() { } if (this->scroll_mode_ == ScrollMode::STOP) { - if (this->stepsleft_ + get_width_internal() == first_line_size + 1) { + if (static_cast(this->stepsleft_ + get_width_internal()) == first_line_size + 1) { if (millis_since_last_scroll < this->scroll_dwell_) { ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.", this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_); diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index d40a64b68e..1a17715315 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -20,6 +20,23 @@ bool MCP2515::setup_internal() { return false; if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) 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) return false; uint8_t err_flags = this->get_error_flags_(); diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index ce0241677d..6e148092fe 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -58,21 +58,13 @@ CONFIG_SCHEMA = cv.All( ) -def mdns_txt_record(key: str, value: str): - return cg.StructInitializer( - MDNSTXTRecord, - ("key", key), - ("value", value), - ) - - def mdns_service( service: str, proto: str, port: int, txt_records: list[dict[str, str]] ): return cg.StructInitializer( MDNSService, - ("service_type", service), - ("proto", proto), + ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")), + ("proto", cg.RawExpression(f"MDNS_STR({cg.safe_exp(proto)})")), ("port", port), ("txt_records", txt_records), ) @@ -107,23 +99,53 @@ async def to_code(config): # Ensure at least 1 service (fallback service) cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) + # Calculate compile-time dynamic TXT value count + # Dynamic values are those that cannot be stored in flash at compile time + dynamic_txt_count = 0 + if "api" in CORE.config: + # Always: get_mac_address() + dynamic_txt_count += 1 + # User-provided templatable TXT values (only lambdas, not static strings) + dynamic_txt_count += sum( + 1 + for service in config[CONF_SERVICES] + for txt_value in service[CONF_TXT].values() + if cg.is_template(txt_value) + ) + + # Ensure at least 1 to avoid zero-size array + cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count)) + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for service in config[CONF_SERVICES]: - txt = [ - cg.StructInitializer( - MDNSTXTRecord, - ("key", txt_key), - ("value", await cg.templatable(txt_value, [], cg.std_string)), - ) - for txt_key, txt_value in service[CONF_TXT].items() - ] + # Build the txt records list for the service + txt_records = [] + for txt_key, txt_value in service[CONF_TXT].items(): + if cg.is_template(txt_value): + # It's a lambda - evaluate and store using helper + templated_value = await cg.templatable(txt_value, [], cg.std_string) + safe_key = cg.safe_exp(txt_key) + dynamic_call = f"{var}->add_dynamic_txt_value(({templated_value})())" + txt_records.append( + cg.RawExpression( + f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}" + ) + ) + else: + # It's a static string - use directly in flash, no need to store in vector + txt_records.append( + cg.RawExpression( + f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({cg.safe_exp(txt_value)})}}" + ) + ) + exp = mdns_service( service[CONF_SERVICE], service[CONF_PROTOCOL], await cg.templatable(service[CONF_PORT], [], cg.uint16), - txt, + txt_records, ) cg.add(var.add_extra_service(exp)) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index e22bba16f6..9cb664c3c3 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -9,24 +9,9 @@ #include // Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms #define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value -// Helper to get string from PROGMEM - returns a temporary std::string -// Only define this function if we have services that will use it -#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES) -static std::string mdns_string_p(const char *src) { - char buf[64]; - strncpy_P(buf, src, sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - return std::string(buf); -} -#define MDNS_STR(name) mdns_string_p(name) -#else -// If no services are configured, we still need the fallback service but it uses string literals -#define MDNS_STR(name) std::string(name) -#endif #else // On non-ESP8266 platforms, use regular const char* -#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char *name = value -#define MDNS_STR(name) name +#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value #endif #ifdef USE_API @@ -46,30 +31,10 @@ static const char *const TAG = "mdns"; #endif // Define all constant strings using the macro -MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); -MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http"); -MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); -MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); -MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); -MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); -MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); -MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); -MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network"); -MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption"); -MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported"); -MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name"); -MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version"); -MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url"); - -MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); -MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32"); -MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040"); - -MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi"); -MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet"); -MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); +// Wrap build-time defines into flash storage +MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); void MDNSComponent::compile_records_() { this->hostname_ = App.get_name(); @@ -78,8 +43,17 @@ void MDNSComponent::compile_records_() { // in mdns/__init__.py. If you add a new service here, update both locations. #ifdef USE_API + MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); + MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); + MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); + MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); + MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); + MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); + MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network"); + MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD); + if (api::global_api_server != nullptr) { - auto &service = this->services_[this->services_.count()++]; + auto &service = this->services_.emplace_next(); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.proto = MDNS_STR(SERVICE_TCP); service.port = api::global_api_server->get_port(); @@ -112,73 +86,92 @@ void MDNSComponent::compile_records_() { txt_records.reserve(txt_count); if (!friendly_name_empty) { - txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name}); + txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())}); } - txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); - txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); + txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}); + txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))}); #ifdef USE_ESP8266 + MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)}); #elif defined(USE_ESP32) + MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32"); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)}); #elif defined(USE_RP2040) + MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040"); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)}); #elif defined(USE_LIBRETINY) - txt_records.emplace_back(MDNSTXTRecord{"platform", lt_cpu_get_model_name()}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())}); #endif - txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD}); + txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)}); #if defined(USE_WIFI) + MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi"); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); #elif defined(USE_ETHERNET) + MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet"); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)}); #elif defined(USE_OPENTHREAD) + MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)}); #endif #ifdef USE_API_NOISE + MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption"); + MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported"); MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256"); - if (api::global_api_server->get_noise_ctx()->has_psk()) { - txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)}); - } else { - txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)}); - } + bool has_psk = api::global_api_server->get_noise_ctx()->has_psk(); + const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; + txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif #ifdef ESPHOME_PROJECT_NAME - txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); - txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); + MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name"); + MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version"); + MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME); + MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION); + txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)}); #endif // ESPHOME_PROJECT_NAME #ifdef USE_DASHBOARD_IMPORT - txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()}); + MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url"); + txt_records.push_back( + {MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())}); #endif } #endif // USE_API #ifdef USE_PROMETHEUS - auto &prom_service = this->services_[this->services_.count()++]; + MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http"); + + auto &prom_service = this->services_.emplace_next(); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER - auto &web_service = this->services_[this->services_.count()++]; + MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); + + auto &web_service = this->services_.emplace_next(); web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.proto = MDNS_STR(SERVICE_TCP); web_service.port = USE_WEBSERVER_PORT; #endif #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) + MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); + MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); + // Publish "http" service if not using native API or any other services // This is just to have *some* mDNS service so that .local resolution works - auto &fallback_service = this->services_[this->services_.count()++]; - fallback_service.service_type = "_http"; - fallback_service.proto = "_tcp"; + auto &fallback_service = this->services_.emplace_next(); + fallback_service.service_type = MDNS_STR(SERVICE_HTTP); + fallback_service.proto = MDNS_STR(SERVICE_TCP); fallback_service.port = USE_WEBSERVER_PORT; - fallback_service.txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION}); + fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}); #endif } @@ -190,11 +183,10 @@ void MDNSComponent::dump_config() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { - ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), + ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), const_cast &>(service.port).value()); for (const auto &record : service.txt_records) { - ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(), - const_cast &>(record.value).value().c_str()); + ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index fdbe5b11e7..141e42d976 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -9,21 +9,34 @@ namespace esphome { namespace mdns { +// Helper struct that identifies strings that may be stored in flash storage (similar to LogString) +struct MDNSString; + +// Macro to cast string literals to MDNSString* (works on all platforms) +#define MDNS_STR(name) (reinterpret_cast(name)) + +#ifdef USE_ESP8266 +#include +#define MDNS_STR_ARG(s) ((PGM_P) (s)) +#else +#define MDNS_STR_ARG(s) (reinterpret_cast(s)) +#endif + // Service count is calculated at compile time by Python codegen // MDNS_SERVICE_COUNT will always be defined struct MDNSTXTRecord { - std::string key; - TemplatableValue value; + const MDNSString *key; + const MDNSString *value; }; struct MDNSService { // service name _including_ underscore character prefix // as defined in RFC6763 Section 7 - std::string service_type; + const MDNSString *service_type; // second label indicating protocol _including_ underscore character prefix // as defined in RFC6763 Section 7, like "_tcp" or "_udp" - std::string proto; + const MDNSString *proto; TemplatableValue port; std::vector txt_records; }; @@ -39,13 +52,24 @@ class MDNSComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } #ifdef USE_MDNS_EXTRA_SERVICES - void add_extra_service(MDNSService service) { this->services_[this->services_.count()++] = std::move(service); } + void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); } #endif const StaticVector &get_services() const { return this->services_; } void on_shutdown() override; + /// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord + const char *add_dynamic_txt_value(const std::string &value) { + this->dynamic_txt_values_.push_back(value); + return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str(); + } + + /// Storage for runtime-generated TXT values (MAC address, user lambdas) + /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations. + /// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this. + StaticVector dynamic_txt_values_; + protected: StaticVector services_{}; std::string hostname_; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index ffd86afec1..e77c0b9b05 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,7 +2,6 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include -#include #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -29,23 +28,18 @@ void MDNSComponent::setup() { std::vector txt_records; for (const auto &record : service.txt_records) { mdns_txt_item_t it{}; - // dup strings to ensure the pointer is valid even after the record loop - it.key = strdup(record.key.c_str()); - it.value = strdup(const_cast &>(record.value).value().c_str()); + // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_ + // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies + it.key = MDNS_STR_ARG(record.key); + it.value = MDNS_STR_ARG(record.value); txt_records.push_back(it); } uint16_t port = const_cast &>(service.port).value(); - err = mdns_service_add(nullptr, service.service_type.c_str(), service.proto.c_str(), port, txt_records.data(), - 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) - } + err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, + txt_records.data(), txt_records.size()); 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", MDNS_STR_ARG(service.service_type), esp_err_to_name(err)); } } } diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 2c90d57021..f3779042ed 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -21,19 +21,19 @@ void MDNSComponent::setup() { // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds // the underscore itself. - auto *proto = service.proto.c_str(); - while (*proto == '_') { + auto *proto = MDNS_STR_ARG(service.proto); + while (progmem_read_byte((const uint8_t *) proto) == '_') { proto++; } - auto *service_type = service.service_type.c_str(); - while (*service_type == '_') { + auto *service_type = MDNS_STR_ARG(service.service_type); + while (progmem_read_byte((const uint8_t *) service_type) == '_') { service_type++; } uint16_t port = const_cast &>(service.port).value(); - MDNS.addService(service_type, proto, port); + MDNS.addService(FPSTR(service_type), FPSTR(proto), port); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, record.key.c_str(), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)), + FPSTR(MDNS_STR_ARG(record.value))); } } } diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 7a41ec9dce..5540bf361a 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -21,19 +21,18 @@ void MDNSComponent::setup() { // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds // the underscore itself. - auto *proto = service.proto.c_str(); + auto *proto = MDNS_STR_ARG(service.proto); while (*proto == '_') { proto++; } - auto *service_type = service.service_type.c_str(); + auto *service_type = MDNS_STR_ARG(service.service_type); while (*service_type == '_') { service_type++; } uint16_t port_ = const_cast &>(service.port).value(); MDNS.addService(service_type, proto, port_); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, record.key.c_str(), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } } diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 95894323f4..5ad006f5d4 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -21,19 +21,18 @@ void MDNSComponent::setup() { // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds // the underscore itself. - auto *proto = service.proto.c_str(); + auto *proto = MDNS_STR_ARG(service.proto); while (*proto == '_') { proto++; } - auto *service_type = service.service_type.c_str(); + auto *service_type = MDNS_STR_ARG(service.service_type); while (*service_type == '_') { service_type++; } uint16_t port = const_cast &>(service.port).value(); MDNS.addService(service_type, proto, port); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, record.key.c_str(), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } } diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index f670a5913d..7e687cabaa 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -343,11 +343,7 @@ class DriverChip: ) offset_height = native_height - height - offset_height # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer - rotated = not requires_buffer(config) and config.get(CONF_ROTATION, 0) in ( - 90, - 270, - ) - if transform.get(CONF_SWAP_XY) is True or rotated: + if transform.get(CONF_SWAP_XY) is True: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e891e2daad..52b5b86fba 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -380,25 +380,41 @@ def get_instance(config): bus_type = BusTypes[bus_type] buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 frac = denominator(config) - rotation = DISPLAY_ROTATIONS[ + rotation = ( 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) - ] + ) templateargs = [ buffer_type, bufferpixels, config[CONF_BYTE_ORDER] == "big_endian", display_pixel_mode, bus_type, - width, - height, - offset_width, - offset_height, ] # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi if requires_buffer(config): - templateargs.append(rotation) - templateargs.append(frac) + templateargs.extend( + [ + width, + height, + offset_width, + offset_height, + DISPLAY_ROTATIONS[rotation], + frac, + ] + ) return MipiSpiBuffer, templateargs + # Swap height and width if the display is rotated 90 or 270 degrees in software + if rotation in (90, 270): + width, height = height, width + offset_width, offset_height = offset_height, offset_width + templateargs.extend( + [ + width, + height, + offset_width, + offset_height, + ] + ) return MipiSpi, templateargs diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 00b861f71b..248d5b7104 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -340,7 +340,7 @@ class MipiSpi : public display::Display, this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); } } else { - for (size_t y = 0; y != h; y++) { + for (size_t y = 0; y != static_cast(h); y++) { if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { this->write_array(ptr, w); } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { @@ -372,8 +372,8 @@ class MipiSpi : public display::Display, uint8_t dbuffer[DISPLAYPIXEL * 48]; uint8_t *dptr = dbuffer; auto stride = x_offset + w + x_pad; // stride in pixels - for (size_t y = 0; y != h; y++) { - for (size_t x = 0; x != w; x++) { + for (size_t y = 0; y != static_cast(h); y++) { + for (size_t x = 0; x != static_cast(w); x++) { auto color_val = ptr[y * stride + x]; if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { // 16 to 18 bit conversion diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index fc0517c7be..b0b64f5709 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -572,7 +572,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } } else { // Determine how many frames to mix - for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); @@ -581,7 +581,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); // Mix two streams together - for (int i = 1; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { mix_audio_samples(primary_buffer, primary_stream_info, reinterpret_cast(transfer_buffers_with_data[i]->get_buffer_start()), speakers_with_data[i]->get_audio_stream_info(), @@ -596,7 +596,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { transfer_buffers_with_data[i]->decrease_buffer_length( speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); speakers_with_data[i]->pending_playback_frames_ += frames_to_mix; diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 6350f43ef6..20271b4bdb 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -66,7 +66,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint8_t data_offset = 3; // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes - if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) { + if (((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT) && + (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END)) || + ((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT) && + (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END))) { // Handle user-defined function, since we don't know how big this ought to be, // ideally we should delegate the entire length detection to whatever handler is // installed, but wait, there is the CRC, and if we get a hit there is a good @@ -91,10 +94,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } else { // data starts at 2 and length is 4 for read registers commands if (this->role == ModbusRole::SERVER) { - if (function_code == 0x1 || function_code == 0x3 || function_code == 0x4 || function_code == 0x6) { + if (function_code == ModbusFunctionCode::READ_COILS || + function_code == ModbusFunctionCode::READ_DISCRETE_INPUTS || + function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || + function_code == ModbusFunctionCode::READ_INPUT_REGISTERS || + function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { data_offset = 2; data_len = 4; - } else if (function_code == 0x10) { + } else if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { if (at < 6) { return true; } @@ -104,7 +111,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } } else { // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { + if (function_code == ModbusFunctionCode::WRITE_SINGLE_COIL || + function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { data_offset = 2; data_len = 4; } @@ -112,7 +122,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Error ( msb indicates error ) // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc - if ((function_code & 0x80) == 0x80) { + if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { data_offset = 2; data_len = 1; } @@ -143,10 +153,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { if (device->address_ == address) { found = true; // Is it an error response? - if ((function_code & 0x80) == 0x80) { + if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); if (waiting_for_response != 0) { - device->on_modbus_error(function_code & 0x7F, raw[2]); + device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]); } else { // Ignore modbus exception not related to a pending command ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); @@ -154,12 +164,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { continue; } if (this->role == ModbusRole::SERVER) { - if (function_code == 0x3 || function_code == 0x4) { + if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || + function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) { device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), uint16_t(data[3]) | (uint16_t(data[2]) << 8)); continue; } - if (function_code == 0x6 || function_code == 0x10) { + if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { device->on_modbus_write_registers(function_code, data); continue; } @@ -199,7 +211,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address // Only check max number of registers for standard function codes // Some devices use non standard codes like 0x43 - if (number_of_entities > MAX_VALUES && function_code <= 0x10) { + if (number_of_entities > MAX_VALUES && function_code <= ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES); return; } @@ -210,15 +222,17 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address if (this->role == ModbusRole::CLIENT) { data.push_back(start_address >> 8); data.push_back(start_address >> 0); - if (function_code != 0x5 && function_code != 0x6) { + if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL && + function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) { data.push_back(number_of_entities >> 8); data.push_back(number_of_entities >> 0); } } if (payload != nullptr) { - if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple - data.push_back(payload_len); // Byte count is required for write + if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple + data.push_back(payload_len); // Byte count is required for write } else { payload_len = 2; // Write single register or coil } diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index ec35612690..fac74aaadf 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" +#include "esphome/components/modbus/modbus_definitions.h" + #include namespace esphome { @@ -65,12 +67,12 @@ class ModbusDevice { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); } void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } - void send_error(uint8_t function_code, uint8_t exception_code) { + void send_error(uint8_t function_code, ModbusExceptionCode exception_code) { std::vector error_response; error_response.reserve(3); error_response.push_back(this->address_); - error_response.push_back(function_code | 0x80); - error_response.push_back(exception_code); + error_response.push_back(function_code | FUNCTION_CODE_EXCEPTION_MASK); + error_response.push_back(static_cast(exception_code)); this->send_raw(error_response); } // If more than one device is connected block sending a new command before a response is received diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h new file mode 100644 index 0000000000..07f101ae4c --- /dev/null +++ b/esphome/components/modbus/modbus_definitions.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus { + +/// Modbus definitions from specs: +/// https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf +// 5 Function Code Categories +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT = 65; // 0x41 +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END = 72; // 0x48 + +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT = 100; // 0x64 +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E + +enum class ModbusFunctionCode : uint8_t { + CUSTOM = 0x00, + READ_COILS = 0x01, + READ_DISCRETE_INPUTS = 0x02, + READ_HOLDING_REGISTERS = 0x03, + READ_INPUT_REGISTERS = 0x04, + WRITE_SINGLE_COIL = 0x05, + WRITE_SINGLE_REGISTER = 0x06, + READ_EXCEPTION_STATUS = 0x07, // not implemented + DIAGNOSTICS = 0x08, // not implemented + GET_COMM_EVENT_COUNTER = 0x0B, // not implemented + GET_COMM_EVENT_LOG = 0x0C, // not implemented + WRITE_MULTIPLE_COILS = 0x0F, + WRITE_MULTIPLE_REGISTERS = 0x10, + REPORT_SERVER_ID = 0x11, // not implemented + READ_FILE_RECORD = 0x14, // not implemented + WRITE_FILE_RECORD = 0x15, // not implemented + MASK_WRITE_REGISTER = 0x16, // not implemented + READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented + READ_FIFO_QUEUE = 0x18, // not implemented +}; + +/*Allow comparison operators between ModbusFunctionCode and uint8_t*/ +inline bool operator==(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) == rhs; } +inline bool operator==(uint8_t lhs, ModbusFunctionCode rhs) { return lhs == static_cast(rhs); } +inline bool operator!=(ModbusFunctionCode lhs, uint8_t rhs) { return !(static_cast(lhs) == rhs); } +inline bool operator!=(uint8_t lhs, ModbusFunctionCode rhs) { return !(lhs == static_cast(rhs)); } +inline bool operator<(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) < rhs; } +inline bool operator<(uint8_t lhs, ModbusFunctionCode rhs) { return lhs < static_cast(rhs); } +inline bool operator<=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) <= rhs; } +inline bool operator<=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs <= static_cast(rhs); } +inline bool operator>(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) > rhs; } +inline bool operator>(uint8_t lhs, ModbusFunctionCode rhs) { return lhs > static_cast(rhs); } +inline bool operator>=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) >= rhs; } +inline bool operator>=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs >= static_cast(rhs); } + +// 4.3 MODBUS Data model +enum class ModbusRegisterType : uint8_t { + CUSTOM = 0x00, + COIL = 0x01, + DISCRETE_INPUT = 0x02, + HOLDING = 0x03, + READ = 0x04, +}; + +// 7 MODBUS Exception Responses: +const uint8_t FUNCTION_CODE_MASK = 0x7F; +const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80; + +enum class ModbusExceptionCode : uint8_t { + ILLEGAL_FUNCTION = 0x01, + ILLEGAL_DATA_ADDRESS = 0x02, + ILLEGAL_DATA_VALUE = 0x03, + SERVICE_DEVICE_FAILURE = 0x04, + ACKNOWLEDGE = 0x05, + SERVER_DEVICE_BUSY = 0x06, + MEMORY_PARITY_ERROR = 0x08, + GATEWAY_PATH_UNAVAILABLE = 0x0A, + GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B, +}; + +// 6.12 16 (0x10) Write Multiple registers: +const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B + +// 6.3 03 (0x03) Read Holding Registers +// 6.4 04 (0x04) Read Input Registers +const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D +/// End of Modbus definitions +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 5ab82f5e17..28f3326c47 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -20,6 +20,7 @@ from .const import ( CONF_BYTE_OFFSET, CONF_COMMAND_THROTTLE, CONF_CUSTOM_COMMAND, + CONF_ENABLED, CONF_FORCE_NEW_RANGE, CONF_MAX_CMD_RETRIES, CONF_MODBUS_CONTROLLER_ID, @@ -28,8 +29,11 @@ from .const import ( CONF_ON_OFFLINE, CONF_ON_ONLINE, CONF_REGISTER_COUNT, + CONF_REGISTER_LAST_ADDRESS, CONF_REGISTER_TYPE, + CONF_REGISTER_VALUE, CONF_RESPONSE_SIZE, + CONF_SERVER_COURTESY_RESPONSE, CONF_SKIP_UPDATES, CONF_VALUE_TYPE, ) @@ -49,6 +53,7 @@ ModbusController = modbus_controller_ns.class_( ) SensorItem = modbus_controller_ns.struct("SensorItem") +ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") ServerRegister = modbus_controller_ns.struct("ServerRegister") ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") @@ -143,6 +148,14 @@ ModbusOfflineTrigger = modbus_controller_ns.class_( _LOGGER = logging.getLogger(__name__) +SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ENABLED, default=False): cv.boolean, + cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t, + cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t, + } +) + ModbusServerRegisterSchema = cv.Schema( { cv.GenerateID(): cv.declare_id(ServerRegister), @@ -162,6 +175,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_COMMAND_THROTTLE, default="0ms" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA, cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, cv.Optional( @@ -232,7 +246,7 @@ def validate_modbus_register(config): def _final_validate(config): - if CONF_SERVER_REGISTERS in config: + if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config: return modbus.final_validate_modbus_device("modbus_controller", role="server")( config ) @@ -299,6 +313,20 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE): + cg.add( + var.set_server_courtesy_response( + cg.StructInitializer( + ServerCourtesyResponse, + ("enabled", server_courtesy_response[CONF_ENABLED]), + ( + "register_last_address", + server_courtesy_response[CONF_REGISTER_LAST_ADDRESS], + ), + ("register_value", server_courtesy_response[CONF_REGISTER_VALUE]), + ) + ) + ) cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) if CONF_SERVER_REGISTERS in config: diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 4d39e48dcd..ee0b5fc633 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -2,6 +2,7 @@ CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands" CONF_BITMASK = "bitmask" CONF_BYTE_OFFSET = "byte_offset" CONF_COMMAND_THROTTLE = "command_throttle" +CONF_ENABLED = "enabled" CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" CONF_CUSTOM_COMMAND = "custom_command" CONF_FORCE_NEW_RANGE = "force_new_range" @@ -13,8 +14,11 @@ CONF_ON_ONLINE = "on_online" CONF_ON_OFFLINE = "on_offline" CONF_RAW_ENCODE = "raw_encode" CONF_REGISTER_COUNT = "register_count" +CONF_REGISTER_LAST_ADDRESS = "register_last_address" CONF_REGISTER_TYPE = "register_type" +CONF_REGISTER_VALUE = "register_value" CONF_RESPONSE_SIZE = "response_size" +CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response" CONF_SKIP_UPDATES = "skip_updates" CONF_USE_WRITE_MULTIPLE = "use_write_multiple" CONF_VALUE_TYPE = "value_type" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 0f3ddf920d..50bd9f45cb 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -112,6 +112,12 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t "0x%X.", this->address_, function_code, start_address, number_of_registers); + if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) { + ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return; + } + std::vector sixteen_bit_response; for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { bool found = false; @@ -136,9 +142,21 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t } if (!found) { - ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); - send_error(function_code, 0x02); - return; + if (this->server_courtesy_response_.enabled && + (current_address <= this->server_courtesy_response_.register_last_address)) { + ESP_LOGD(TAG, + "Could not match any register to address 0x%02X, but default allowed. " + "Returning default value: %d.", + current_address, this->server_courtesy_response_.register_value); + sixteen_bit_response.push_back(this->server_courtesy_response_.register_value); + current_address += 1; // Just increment by 1, as the default response is a single register + } else { + ESP_LOGW(TAG, + "Could not match any register to address 0x%02X and default not allowed. Sending exception response.", + current_address); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return; + } } } @@ -156,27 +174,27 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st uint16_t number_of_registers; uint16_t payload_offset; - if (function_code == 0x10) { + if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); - if (number_of_registers == 0 || number_of_registers > 0x7B) { + if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); - send_error(function_code, 3); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } uint16_t payload_size = data[4]; if (payload_size != number_of_registers * 2) { ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", payload_size, number_of_registers); - send_error(function_code, 3); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } payload_offset = 5; - } else if (function_code == 0x06) { + } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { number_of_registers = 1; payload_offset = 2; } else { ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); - send_error(function_code, 1); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); return; } @@ -211,7 +229,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { return server_register->write_lambda != nullptr; })) { - send_error(function_code, 1); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); return; } @@ -220,7 +238,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); return server_register->write_lambda(number); })) { - send_error(function_code, 4); + this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); return; } @@ -431,8 +449,15 @@ void ModbusController::dump_config() { "ModbusController:\n" " Address: 0x%02X\n" " Max Command Retries: %d\n" - " Offline Skip Updates: %d", - this->address_, this->max_cmd_retries_, this->offline_skip_updates_); + " Offline Skip Updates: %d\n" + " Server Courtesy Response:\n" + " Enabled: %s\n" + " Register Last Address: 0x%02X\n" + " Register Value: %d", + this->address_, this->max_cmd_retries_, this->offline_skip_updates_, + this->server_courtesy_response_.enabled ? "true" : "false", + this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value); + #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : this->sensorset_) { diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index a86ad1ccb5..6ed05715cb 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -16,35 +16,9 @@ namespace modbus_controller { class ModbusController; -enum class ModbusFunctionCode { - CUSTOM = 0x00, - READ_COILS = 0x01, - READ_DISCRETE_INPUTS = 0x02, - READ_HOLDING_REGISTERS = 0x03, - READ_INPUT_REGISTERS = 0x04, - WRITE_SINGLE_COIL = 0x05, - WRITE_SINGLE_REGISTER = 0x06, - READ_EXCEPTION_STATUS = 0x07, // not implemented - DIAGNOSTICS = 0x08, // not implemented - GET_COMM_EVENT_COUNTER = 0x0B, // not implemented - GET_COMM_EVENT_LOG = 0x0C, // not implemented - WRITE_MULTIPLE_COILS = 0x0F, - WRITE_MULTIPLE_REGISTERS = 0x10, - REPORT_SERVER_ID = 0x11, // not implemented - READ_FILE_RECORD = 0x14, // not implemented - WRITE_FILE_RECORD = 0x15, // not implemented - MASK_WRITE_REGISTER = 0x16, // not implemented - READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented - READ_FIFO_QUEUE = 0x18, // not implemented -}; - -enum class ModbusRegisterType : uint8_t { - CUSTOM = 0x0, - COIL = 0x01, - DISCRETE_INPUT = 0x02, - HOLDING = 0x03, - READ = 0x04, -}; +using modbus::ModbusFunctionCode; +using modbus::ModbusRegisterType; +using modbus::ModbusExceptionCode; enum class SensorValueType : uint8_t { RAW = 0x00, // variable length @@ -256,6 +230,12 @@ class SensorItem { bool force_new_range{false}; }; +struct ServerCourtesyResponse { + bool enabled{false}; + uint16_t register_last_address{0xFFFF}; + uint16_t register_value{0}; +}; + class ServerRegister { using ReadLambda = std::function; using WriteLambda = std::function; @@ -530,6 +510,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } /// get how many times a command will be (re)sent if no response is received uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; } + /// Called by esphome generated code to set the server courtesy response object + void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) { + this->server_courtesy_response_ = server_courtesy_response; + } + /// Get the server courtesy response object + ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; } protected: /// parse sensormap_ and create range of sequential addresses @@ -572,6 +558,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { CallbackManager online_callback_{}; /// Server offline callback CallbackManager offline_callback_{}; + /// Server courtesy response + ServerCourtesyResponse server_courtesy_response_{ + .enabled = false, .register_last_address = 0xFFFF, .register_value = 0}; }; /** Convert vector response payload to float. diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 074bc79ea2..5a8a8e7205 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -11,47 +11,49 @@ namespace mpr121 { static const char *const TAG = "mpr121"; void MPR121Component::setup() { + this->disable_loop(); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); - delay(100); // NOLINT - if (!this->write_byte(MPR121_ECR, 0x0)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } + this->set_timeout(100, [this]() { + if (!this->write_byte(MPR121_ECR, 0x0)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + // set touch sensitivity for all 12 channels + for (auto *channel : this->channels_) { + channel->setup(); + } + this->write_byte(MPR121_MHDR, 0x01); + this->write_byte(MPR121_NHDR, 0x01); + this->write_byte(MPR121_NCLR, 0x0E); + this->write_byte(MPR121_FDLR, 0x00); - // set touch sensitivity for all 12 channels - for (auto *channel : this->channels_) { - channel->setup(); - } - this->write_byte(MPR121_MHDR, 0x01); - this->write_byte(MPR121_NHDR, 0x01); - this->write_byte(MPR121_NCLR, 0x0E); - this->write_byte(MPR121_FDLR, 0x00); + this->write_byte(MPR121_MHDF, 0x01); + this->write_byte(MPR121_NHDF, 0x05); + this->write_byte(MPR121_NCLF, 0x01); + this->write_byte(MPR121_FDLF, 0x00); - this->write_byte(MPR121_MHDF, 0x01); - this->write_byte(MPR121_NHDF, 0x05); - this->write_byte(MPR121_NCLF, 0x01); - this->write_byte(MPR121_FDLF, 0x00); + this->write_byte(MPR121_NHDT, 0x00); + this->write_byte(MPR121_NCLT, 0x00); + this->write_byte(MPR121_FDLT, 0x00); - this->write_byte(MPR121_NHDT, 0x00); - this->write_byte(MPR121_NCLT, 0x00); - this->write_byte(MPR121_FDLT, 0x00); + this->write_byte(MPR121_DEBOUNCE, 0); + // default, 16uA charge current + this->write_byte(MPR121_CONFIG1, 0x10); + // 0.5uS encoding, 1ms period + this->write_byte(MPR121_CONFIG2, 0x20); - this->write_byte(MPR121_DEBOUNCE, 0); - // default, 16uA charge current - this->write_byte(MPR121_CONFIG1, 0x10); - // 0.5uS encoding, 1ms period - this->write_byte(MPR121_CONFIG2, 0x20); + // Write the Electrode Configuration Register + // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. + // * The 2 bits below is "Proximity Enable" and are left at 0. + // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled + // as a range, starting at 0 up to the highest channel index used. + this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); - // Write the Electrode Configuration Register - // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. - // * The 2 bits below is "Proximity Enable" and are left at 0. - // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled - // as a range, starting at 0 up to the highest channel index used. - this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); - - this->flush_gpio_(); + this->flush_gpio_(); + this->enable_loop(); + }); } void MPR121Component::set_touch_debounce(uint8_t debounce) { @@ -73,9 +75,6 @@ void MPR121Component::dump_config() { case COMMUNICATION_FAILED: ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; - case WRONG_CHIP_STATE: - ESP_LOGE(TAG, "MPR121 has wrong default value for CONFIG2?"); - break; case NONE: default: break; diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index eb2e2edc57..6dd2c38309 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -88,7 +88,6 @@ class MPR121Component : public Component, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, - WRONG_CHIP_STATE, } error_code_{NONE}; bool flush_gpio_(); diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index acdca03fdb..6a31b754f7 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -218,7 +218,7 @@ void NAU7802Sensor::dump_config() { void NAU7802Sensor::write_value_(uint8_t start_reg, size_t size, int32_t value) { uint8_t data[4]; - for (int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { data[i] = 0xFF & (value >> (size - 1 - i) * 8); } this->write_register(start_reg, data, size); @@ -228,7 +228,7 @@ int32_t NAU7802Sensor::read_value_(uint8_t start_reg, size_t size) { uint8_t data[4]; this->read_register(start_reg, data, size); int32_t result = 0; - for (int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { result |= data[i] << (size - 1 - i) * 8; } // extend sign bit diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index b348bc9920..0ce9d02e97 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -77,7 +77,7 @@ bool Nextion::check_connect_() { this->recv_ret_string_(response, 0, false); if (!response.empty() && response[0] == 0x1A) { // Swallow invalid variable name responses that may be caused by the above commands - ESP_LOGD(TAG, "0x1A error ignored (setup)"); + ESP_LOGV(TAG, "0x1A error ignored (setup)"); return false; } if (response.empty() || response.find("comok") == std::string::npos) { @@ -334,7 +334,7 @@ void Nextion::loop() { this->started_ms_ = App.get_loop_component_start_time(); if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { - ESP_LOGD(TAG, "Manual ready set"); + ESP_LOGV(TAG, "Manual ready set"); this->connection_state_.nextion_reports_is_setup_ = true; } } @@ -544,7 +544,7 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); + ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } @@ -559,7 +559,7 @@ void Nextion::process_nextion_commands_() { } uint8_t page_id = to_process[0]; - ESP_LOGD(TAG, "New page: %u", page_id); + ESP_LOGV(TAG, "New page: %u", page_id); this->page_callback_.call(page_id); break; } @@ -577,7 +577,7 @@ void Nextion::process_nextion_commands_() { const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); + ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); break; } @@ -676,7 +676,7 @@ void Nextion::process_nextion_commands_() { } case 0x88: // system successful start up { - ESP_LOGD(TAG, "System start: %zu", to_process_length); + ESP_LOGV(TAG, "System start: %zu", to_process_length); this->connection_state_.nextion_reports_is_setup_ = true; break; } @@ -922,7 +922,7 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s } void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { - ESP_LOGD(TAG, "State: %s='%s'", name.c_str(), state.c_str()); + ESP_LOGV(TAG, "State: %s='%s'", name.c_str(), state.c_str()); for (auto *sensor : this->textsensortype_) { if (name == sensor->get_variable_name()) { @@ -933,7 +933,7 @@ void Nextion::set_nextion_text_state(const std::string &name, const std::string } void Nextion::all_components_send_state_(bool force_update) { - ESP_LOGD(TAG, "Send states"); + ESP_LOGV(TAG, "Send states"); for (auto *binarysensortype : this->binarysensortype_) { if (force_update || binarysensortype->get_needs_to_send_update()) binarysensortype->send_state_to_nextion(); diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp index f55c9f1813..676a2efca9 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/online_image/bmp_image.cpp @@ -117,7 +117,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->paint_index_++; this->current_index_ += 3; index += 3; - if (x == this->width_ - 1 && this->padding_bytes_ > 0) { + size_t last_col = static_cast(this->width_) - 1; + if (x == last_col && this->padding_bytes_ > 0) { index += this->padding_bytes_; this->current_index_ += this->padding_bytes_; } diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/online_image/jpeg_image.cpp index e5ee3dd8bf..10586091d5 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/online_image/jpeg_image.cpp @@ -25,8 +25,10 @@ static int draw_callback(JPEGDRAW *jpeg) { // to avoid crashing. App.feed_wdt(); size_t position = 0; - for (size_t y = 0; y < jpeg->iHeight; y++) { - for (size_t x = 0; x < jpeg->iWidth; x++) { + size_t height = static_cast(jpeg->iHeight); + size_t width = static_cast(jpeg->iWidth); + for (size_t y = 0; y < height; y++) { + for (size_t x = 0; x < width; x++) { auto rg = decode_value(jpeg->pPixels[position++]); auto ba = decode_value(jpeg->pPixels[position++]); Color color(rg[1], rg[0], ba[1], ba[0]); diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 57b972d195..b2c2519c08 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -155,7 +155,7 @@ void OpenThreadSrpComponent::setup() { // Set service name char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size); - std::string full_service = service.service_type + "." + service.proto; + std::string full_service = std::string(MDNS_STR_ARG(service.service_type)) + "." + MDNS_STR_ARG(service.proto); if (full_service.size() > size) { ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str()); continue; @@ -180,10 +180,12 @@ void OpenThreadSrpComponent::setup() { entry->mService.mNumTxtEntries = service.txt_records.size(); for (size_t i = 0; i < service.txt_records.size(); i++) { const auto &txt = service.txt_records[i]; - auto value = const_cast &>(txt.value).value(); - txt_entries[i].mKey = strdup(txt.key.c_str()); - txt_entries[i].mValue = reinterpret_cast(strdup(value.c_str())); - txt_entries[i].mValueLength = value.size(); + // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_ + // OpenThread SRP client expects the data to persist, so we strdup it + const char *value_str = MDNS_STR_ARG(txt.value); + txt_entries[i].mKey = MDNS_STR_ARG(txt.key); + txt_entries[i].mValue = reinterpret_cast(strdup(value_str)); + txt_entries[i].mValueLength = strlen(value_str); } entry->mService.mTxtEntries = txt_entries; entry->mService.mNumTxtEntries = service.txt_records.size(); diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 1a16f14542..5d7aecdb05 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -104,7 +104,7 @@ float PIDController::weighted_average_(std::deque &list, float new_value, list.push_front(new_value); // keep only 'samples' readings, by popping off the back of the list - while (list.size() > samples) + while (samples > 0 && list.size() > static_cast(samples)) list.pop_back(); // calculate and return the average of all values in the list diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 2677860c7c..68ef18e5ce 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -110,21 +110,21 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) { void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) { if (!area.empty()) { - stream->print(F("\",area=\"")); + stream->print(ESPHOME_F("\",area=\"")); stream->print(area.c_str()); } } void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) { if (!node.empty()) { - stream->print(F("\",node=\"")); + stream->print(ESPHOME_F("\",node=\"")); stream->print(node.c_str()); } } void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) { if (!friendly_name.empty()) { - stream->print(F("\",friendly_name=\"")); + stream->print(ESPHOME_F("\",friendly_name=\"")); stream->print(friendly_name.c_str()); } } @@ -132,8 +132,8 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_sensor_failed gauge\n")); } void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -141,37 +141,37 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor return; if (!std::isnan(obj->state)) { // We have a valid value, output this value - stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",unit=\"")); + stream->print(ESPHOME_F("\",unit=\"")); stream->print(obj->get_unit_of_measurement().c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif @@ -179,8 +179,8 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor // Type-specific implementation #ifdef USE_BINARY_SENSOR void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_binary_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_failed gauge\n")); } void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -188,204 +188,204 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_FAN void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_fan_value gauge\n")); - stream->print(F("#TYPE esphome_fan_failed gauge\n")); - stream->print(F("#TYPE esphome_fan_speed gauge\n")); - stream->print(F("#TYPE esphome_fan_oscillation gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_speed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_oscillation gauge\n")); } void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_fan_failed{id=\"")); + stream->print(ESPHOME_F("esphome_fan_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_fan_value{id=\"")); + stream->print(ESPHOME_F("esphome_fan_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Speed if available if (obj->get_traits().supports_speed()) { - stream->print(F("esphome_fan_speed{id=\"")); + stream->print(ESPHOME_F("esphome_fan_speed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->speed); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } // Oscillation if available if (obj->get_traits().supports_oscillation()) { - stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(ESPHOME_F("esphome_fan_oscillation{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->oscillating); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } #endif #ifdef USE_LIGHT void PrometheusHandler::light_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_light_state gauge\n")); - stream->print(F("#TYPE esphome_light_color gauge\n")); - stream->print(F("#TYPE esphome_light_effect_active gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_state gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_color gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_effect_active gauge\n")); } void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; // State - stream->print(F("esphome_light_state{id=\"")); + stream->print(ESPHOME_F("esphome_light_state{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->remote_values.is_on()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Brightness and RGBW light::LightColorValues color = obj->current_values; float brightness, r, g, b, w; color.as_brightness(&brightness); color.as_rgbw(&r, &g, &b, &w); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"brightness\"} ")); + stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); stream->print(brightness); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"r\"} ")); + stream->print(ESPHOME_F("\",channel=\"r\"} ")); stream->print(r); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"g\"} ")); + stream->print(ESPHOME_F("\",channel=\"g\"} ")); stream->print(g); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"b\"} ")); + stream->print(ESPHOME_F("\",channel=\"b\"} ")); stream->print(b); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"w\"} ")); + stream->print(ESPHOME_F("\",channel=\"w\"} ")); stream->print(w); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Effect std::string effect = obj->get_effect_name(); if (effect == "None") { - stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",effect=\"None\"} 0\n")); + stream->print(ESPHOME_F("\",effect=\"None\"} 0\n")); } else { - stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",effect=\"")); + stream->print(ESPHOME_F("\",effect=\"")); stream->print(effect.c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_COVER void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_cover_value gauge\n")); - stream->print(F("#TYPE esphome_cover_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_cover_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_cover_failed gauge\n")); } void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -393,118 +393,118 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob return; if (!std::isnan(obj->position)) { // We have a valid value, output this value - stream->print(F("esphome_cover_failed{id=\"")); + stream->print(ESPHOME_F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_cover_value{id=\"")); + stream->print(ESPHOME_F("esphome_cover_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->position); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); if (obj->get_traits().get_supports_tilt()) { - stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(ESPHOME_F("esphome_cover_tilt{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->tilt); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } else { // Invalid state - stream->print(F("esphome_cover_failed{id=\"")); + stream->print(ESPHOME_F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_SWITCH void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_switch_value gauge\n")); - stream->print(F("#TYPE esphome_switch_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_switch_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_switch_failed gauge\n")); } void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_switch_failed{id=\"")); + stream->print(ESPHOME_F("esphome_switch_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_switch_value{id=\"")); + stream->print(ESPHOME_F("esphome_switch_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif #ifdef USE_LOCK void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_lock_value gauge\n")); - stream->print(F("#TYPE esphome_lock_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_lock_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_lock_failed gauge\n")); } void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_lock_failed{id=\"")); + stream->print(ESPHOME_F("esphome_lock_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_lock_value{id=\"")); + stream->print(ESPHOME_F("esphome_lock_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif // Type-specific implementation #ifdef USE_TEXT_SENSOR void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_text_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_text_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_sensor_failed gauge\n")); } void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -512,37 +512,37 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_text_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_text_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(obj->state.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_text_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif @@ -550,8 +550,8 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso // Type-specific implementation #ifdef USE_NUMBER void PrometheusHandler::number_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_number_value gauge\n")); - stream->print(F("#TYPE esphome_number_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_number_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_number_failed gauge\n")); } void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -559,43 +559,43 @@ void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number return; if (!std::isnan(obj->state)) { // We have a valid value, output this value - stream->print(F("esphome_number_failed{id=\"")); + stream->print(ESPHOME_F("esphome_number_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_number_value{id=\"")); + stream->print(ESPHOME_F("esphome_number_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_number_failed{id=\"")); + stream->print(ESPHOME_F("esphome_number_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_SELECT void PrometheusHandler::select_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_select_value gauge\n")); - stream->print(F("#TYPE esphome_select_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_select_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_select_failed gauge\n")); } void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -603,105 +603,105 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_select_failed{id=\"")); + stream->print(ESPHOME_F("esphome_select_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_select_value{id=\"")); + stream->print(ESPHOME_F("esphome_select_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(obj->state.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_select_failed{id=\"")); + stream->print(ESPHOME_F("esphome_select_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_MEDIA_PLAYER void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_media_player_state_value gauge\n")); - stream->print(F("#TYPE esphome_media_player_volume gauge\n")); - stream->print(F("#TYPE esphome_media_player_is_muted gauge\n")); - stream->print(F("#TYPE esphome_media_player_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_state_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_volume gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_is_muted gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_failed gauge\n")); } void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_media_player_failed{id=\"")); + stream->print(ESPHOME_F("esphome_media_player_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_media_player_state_value{id=\"")); + stream->print(ESPHOME_F("esphome_media_player_state_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(media_player::media_player_state_to_string(obj->state)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); - stream->print(F("esphome_media_player_volume{id=\"")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_media_player_volume{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->volume); - stream->print(F("\n")); - stream->print(F("esphome_media_player_is_muted{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_media_player_is_muted{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); if (obj->is_muted()) { - stream->print(F("1.0")); + stream->print(ESPHOME_F("1.0")); } else { - stream->print(F("0.0")); + stream->print(ESPHOME_F("0.0")); } - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif #ifdef USE_UPDATE void PrometheusHandler::update_entity_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_update_entity_state gauge\n")); - stream->print(F("#TYPE esphome_update_entity_info gauge\n")); - stream->print(F("#TYPE esphome_update_entity_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_state gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_info gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_failed gauge\n")); } void PrometheusHandler::handle_update_state_(AsyncResponseStream *stream, update::UpdateState state) { @@ -730,168 +730,168 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update:: return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_update_entity_failed{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // First update state - stream->print(F("esphome_update_entity_state{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_state{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); handle_update_state_(stream, obj->state); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); // Next update info - stream->print(F("esphome_update_entity_info{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_info{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",current_version=\"")); + stream->print(ESPHOME_F("\",current_version=\"")); stream->print(obj->update_info.current_version.c_str()); - stream->print(F("\",latest_version=\"")); + stream->print(ESPHOME_F("\",latest_version=\"")); stream->print(obj->update_info.latest_version.c_str()); - stream->print(F("\",title=\"")); + stream->print(ESPHOME_F("\",title=\"")); stream->print(obj->update_info.title.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_update_entity_failed{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_VALVE void PrometheusHandler::valve_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_valve_operation gauge\n")); - stream->print(F("#TYPE esphome_valve_failed gauge\n")); - stream->print(F("#TYPE esphome_valve_position gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_operation gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_position gauge\n")); } void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_valve_failed{id=\"")); + stream->print(ESPHOME_F("esphome_valve_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_valve_operation{id=\"")); + stream->print(ESPHOME_F("esphome_valve_operation{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",operation=\"")); + stream->print(ESPHOME_F("\",operation=\"")); stream->print(valve::valve_operation_to_str(obj->current_operation)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); // Now see if position is supported if (obj->get_traits().get_supports_position()) { - stream->print(F("esphome_valve_position{id=\"")); + stream->print(ESPHOME_F("esphome_valve_position{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->position); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } #endif #ifdef USE_CLIMATE void PrometheusHandler::climate_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_climate_setting gauge\n")); - stream->print(F("#TYPE esphome_climate_value gauge\n")); - stream->print(F("#TYPE esphome_climate_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_setting gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_failed gauge\n")); } void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &setting, const LogString *setting_value) { - stream->print(F("esphome_climate_setting{id=\"")); + stream->print(ESPHOME_F("esphome_climate_setting{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(setting.c_str()); - stream->print(F("\",setting_value=\"")); + stream->print(ESPHOME_F("\",setting_value=\"")); stream->print(LOG_STR_ARG(setting_value)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, std::string &climate_value) { - stream->print(F("esphome_climate_value{id=\"")); + stream->print(ESPHOME_F("esphome_climate_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(climate_value.c_str()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, bool is_failed_value) { - stream->print(F("esphome_climate_failed{id=\"")); + stream->print(ESPHOME_F("esphome_climate_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); if (is_failed_value) { - stream->print(F("1.0")); + stream->print(ESPHOME_F("1.0")); } else { - stream->print(F("0.0")); + stream->print(ESPHOME_F("0.0")); } - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index c9196f2469..d2041a2d52 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -8,6 +8,7 @@ namespace esphome { namespace qmc5883l { static const char *const TAG = "qmc5883l"; + static const uint8_t QMC5883L_ADDRESS = 0x0D; static const uint8_t QMC5883L_REGISTER_DATA_X_LSB = 0x00; @@ -32,6 +33,10 @@ void QMC5883LComponent::setup() { } delay(10); + if (this->drdy_pin_) { + this->drdy_pin_->setup(); + } + uint8_t control_1 = 0; control_1 |= 0b01 << 0; // MODE (Mode) -> 0b00=standby, 0b01=continuous control_1 |= this->datarate_ << 2; @@ -64,6 +69,7 @@ void QMC5883LComponent::setup() { high_freq_.start(); } } + void QMC5883LComponent::dump_config() { ESP_LOGCONFIG(TAG, "QMC5883L:"); LOG_I2C_DEVICE(this); @@ -77,11 +83,20 @@ void QMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_PIN(" DRDY Pin: ", this->drdy_pin_); } + float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } + void QMC5883LComponent::update() { i2c::ErrorCode err; uint8_t status = false; + + // If DRDY pin is configured and the data is not ready return. + if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) { + return; + } + // Status byte gets cleared when data is read, so we have to read this first. // If status and two axes are desired, it's possible to save one byte of traffic by enabling // ROL_PNT in setup and reading 7 bytes starting at the status register. diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 3202e37780..5ba7180e23 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" namespace esphome { namespace qmc5883l { @@ -33,6 +34,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; void update() override; + void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; } void set_range(QMC5883LRange range) { range_ = range; } void set_oversampling(QMC5883LOversampling oversampling) { oversampling_ = oversampling; } @@ -51,6 +53,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *z_sensor_{nullptr}; sensor::Sensor *heading_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; + GPIOPin *drdy_pin_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index ade286cb9e..b79e370a05 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,8 +1,12 @@ +import logging + +from esphome import pins import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, + CONF_DATA_RATE, CONF_FIELD_STRENGTH_X, CONF_FIELD_STRENGTH_Y, CONF_FIELD_STRENGTH_Z, @@ -21,6 +25,10 @@ from esphome.const import ( UNIT_MICROTESLA, ) +_LOGGER = logging.getLogger(__name__) + +CONF_DRDY_PIN = "drdy_pin" + DEPENDENCIES = ["i2c"] qmc5883l_ns = cg.esphome_ns.namespace("qmc5883l") @@ -52,6 +60,18 @@ QMC5883LOversamplings = { } +def validate_config(config): + if ( + config[CONF_UPDATE_INTERVAL].total_milliseconds < 15 + and CONF_DRDY_PIN not in config + ): + _LOGGER.warning( + "[qmc5883l] 'update_interval' is less than 15ms and 'drdy_pin' is " + "not configured, this may result in I2C errors" + ) + return config + + def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: @@ -88,7 +108,7 @@ temperature_schema = sensor.sensor_schema( state_class=STATE_CLASS_MEASUREMENT, ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(QMC5883LComponent), @@ -104,29 +124,25 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, cv.Optional(CONF_HEADING): heading_schema, cv.Optional(CONF_TEMPERATURE): temperature_schema, + cv.Optional(CONF_DRDY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_DATA_RATE, default="200hz"): validate_enum( + QMC5883LDatarates, units=["hz", "Hz"] + ), } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x0D)) + .extend(i2c.i2c_device_schema(0x0D)), + validate_config, ) -def auto_data_rate(config): - interval_sec = config[CONF_UPDATE_INTERVAL].total_milliseconds / 1000 - interval_hz = 1.0 / interval_sec - for datarate in sorted(QMC5883LDatarates.keys()): - if float(datarate) >= interval_hz: - return QMC5883LDatarates[datarate] - return QMC5883LDatarates[200] - - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) - cg.add(var.set_datarate(auto_data_rate(config))) + cg.add(var.set_datarate(config[CONF_DATA_RATE])) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) @@ -143,3 +159,6 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + if CONF_DRDY_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) + cg.add(var.set_drdy_pin(pin)) diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 54e0dff663..4f6de5e59e 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -10,8 +10,8 @@ constexpr uint32_t BIT_MARK_US = 580; // 70us seems like a safe time delta for constexpr uint32_t BIT_ONE_SPACE_US = 1640; constexpr uint32_t BIT_ZERO_SPACE_US = 545; constexpr uint64_t HEADER = 0b011001001100010uL; // 15 bits -constexpr uint64_t HEADER_SIZE = 15; -constexpr uint64_t CODE_SIZE = 17; +constexpr size_t HEADER_SIZE = 15; +constexpr size_t CODE_SIZE = 17; void GoboxProtocol::dump_timings_(const RawTimings &timings) const { ESP_LOGD(TAG, "Gobox: size=%u", timings.size()); @@ -39,7 +39,7 @@ void GoboxProtocol::encode(RemoteTransmitData *dst, const GoboxData &data) { } optional GoboxProtocol::decode(RemoteReceiveData src) { - if (src.size() < ((HEADER_SIZE + CODE_SIZE) * 2 + 1)) { + if (static_cast(src.size()) < ((HEADER_SIZE + CODE_SIZE) * 2 + 1)) { return {}; } diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 956f240b14..cd2b440645 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -5,6 +5,8 @@ from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, CONF_CLOCK_RESOLUTION, CONF_DUMP, CONF_FILTER, @@ -149,6 +151,14 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), 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) @@ -168,6 +178,8 @@ async def to_code(config): cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) if CONF_FILTER_SYMBOLS in config: 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: var = cg.new_Pvariable(config[CONF_ID], pin) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 45e06e664a..3ddcf353c7 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -64,6 +64,8 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, 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_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 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; } @@ -76,6 +78,8 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; bool with_dma_{false}; + uint32_t carrier_frequency_{0}; + uint8_t carrier_duty_percent_{100}; esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 7e1bd3c457..49358eef3f 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -72,6 +72,21 @@ void RemoteReceiverComponent::setup() { 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; memset(&callbacks, 0, sizeof(callbacks)); callbacks.on_recv_done = rmt_callback; @@ -111,11 +126,13 @@ void RemoteReceiverComponent::dump_config() { " Filter symbols: %" PRIu32 "\n" " Receive symbols: %" PRIu32 "\n" " Tolerance: %" PRIu32 "%s\n" + " Carrier frequency: %" PRIu32 " hz\n" + " Carrier duty: %u%%\n" " Filter out pulses shorter than: %" PRIu32 " us\n" " Signal is done after %" PRIu32 " us of no changes", this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", - this->filter_us_, this->idle_us_); + this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); if (this->is_failed()) { ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), this->error_string_.c_str()); diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 2c48105490..b79f27e2e5 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -215,7 +215,7 @@ void Rtttl::loop() { sample[x].right = 0; } - if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { + if (static_cast(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { break; } this->samples_sent_++; diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 199460acbc..32abbd739d 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -10,6 +10,39 @@ namespace esphome::sha256 { #if defined(USE_ESP32) || defined(USE_LIBRETINY) +// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: +// +// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains +// internal state that the DMA engine references. This imposes two critical constraints: +// +// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to +// write to incorrect memory locations. This results in null pointer dereferences and crashes. +// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]). +// +// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same +// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack +// frame changes (function call/return), the DMA references become invalid and will produce +// truncated hash output (20 bytes instead of 32) or corrupt memory. +// +// CORRECT USAGE: +// void my_function() { +// sha256::SHA256 hasher; // Created locally +// hasher.init(); +// hasher.add(data, len); // Any size, no chunking needed +// hasher.calculate(); +// bool ok = hasher.equals_hex(expected); +// // hasher destroyed when function returns +// } +// +// INCORRECT USAGE (WILL FAIL ON ESP32-S3): +// void my_function() { +// sha256::SHA256 hasher; +// helper(&hasher); // WRONG: Passed to different stack frame +// } +// void helper(HashBase *h) { +// h->init(); // WRONG: Will produce truncated/corrupted output +// } + SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); } void SHA256::init() { diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index bb089bc314..a2b62799e1 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -39,6 +39,10 @@ class SHA256 : public esphome::HashBase { protected: #if defined(USE_ESP32) || defined(USE_LIBRETINY) + // CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration. + // The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another + // function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce + // truncated/incorrect hash results. mbedtls_sha256_context ctx_{}; #elif defined(USE_ESP8266) || defined(USE_RP2040) br_sha256_context ctx_{}; diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index e3d55681c5..cd09f31dd7 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -50,7 +50,7 @@ static const char *const TAG = "sonoff_d1"; uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { uint8_t crc = 0; - for (int i = 2; i < len - 1; i++) { + for (size_t i = 2; i < len - 1; i++) { crc += cmd[i]; } return crc; diff --git a/esphome/components/split_buffer/__init__.py b/esphome/components/split_buffer/__init__.py new file mode 100644 index 0000000000..be7472936f --- /dev/null +++ b/esphome/components/split_buffer/__init__.py @@ -0,0 +1,5 @@ +CODEOWNERS = ["@jesserockz"] + +# Allows split_buffer to be configured in yaml, to allow use of the C++ api. + +CONFIG_SCHEMA = {} diff --git a/esphome/components/split_buffer/split_buffer.cpp b/esphome/components/split_buffer/split_buffer.cpp new file mode 100644 index 0000000000..a710670a5d --- /dev/null +++ b/esphome/components/split_buffer/split_buffer.cpp @@ -0,0 +1,133 @@ +#include "split_buffer.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::split_buffer { + +static constexpr const char *const TAG = "split_buffer"; + +SplitBuffer::~SplitBuffer() { this->free(); } + +bool SplitBuffer::init(size_t total_length) { + this->free(); // Clean up any existing allocation + + if (total_length == 0) { + return false; + } + + this->total_length_ = total_length; + size_t current_buffer_size = total_length; + + RAMAllocator ptr_allocator; + RAMAllocator allocator; + + // Try to allocate the entire buffer first + while (current_buffer_size > 0) { + // Calculate how many buffers we need of this size + size_t needed_buffers = (total_length + current_buffer_size - 1) / current_buffer_size; + + // Try to allocate array of buffer pointers + uint8_t **temp_buffers = ptr_allocator.allocate(needed_buffers); + if (temp_buffers == nullptr) { + // If we can't even allocate the pointer array, don't need to continue + ESP_LOGE(TAG, "Failed to allocate pointers"); + return false; + } + + // Initialize all pointers to null + for (size_t i = 0; i < needed_buffers; i++) { + temp_buffers[i] = nullptr; + } + + // Try to allocate all the buffers + bool allocation_success = true; + for (size_t i = 0; i < needed_buffers; i++) { + size_t this_buffer_size = current_buffer_size; + // Last buffer might be smaller if total_length is not divisible by current_buffer_size + if (i == needed_buffers - 1 && total_length % current_buffer_size != 0) { + this_buffer_size = total_length % current_buffer_size; + } + + temp_buffers[i] = allocator.allocate(this_buffer_size); + if (temp_buffers[i] == nullptr) { + allocation_success = false; + break; + } + + // Initialize buffer to zero + memset(temp_buffers[i], 0, this_buffer_size); + } + + if (allocation_success) { + // Success! Store the result + this->buffers_ = temp_buffers; + this->buffer_count_ = needed_buffers; + this->buffer_size_ = current_buffer_size; + ESP_LOGD(TAG, "Allocated %zu * %zu bytes - %zu bytes", this->buffer_count_, this->buffer_size_, + this->total_length_); + return true; + } + + // Allocation failed, clean up and try smaller buffers + for (size_t i = 0; i < needed_buffers; i++) { + if (temp_buffers[i] != nullptr) { + allocator.deallocate(temp_buffers[i], 0); + } + } + ptr_allocator.deallocate(temp_buffers, 0); + + // Halve the buffer size and try again + current_buffer_size = current_buffer_size / 2; + } + + ESP_LOGE(TAG, "Failed to allocate %zu bytes", total_length); + return false; +} + +void SplitBuffer::free() { + if (this->buffers_ != nullptr) { + RAMAllocator allocator; + for (size_t i = 0; i < this->buffer_count_; i++) { + if (this->buffers_[i] != nullptr) { + allocator.deallocate(this->buffers_[i], 0); + } + } + RAMAllocator ptr_allocator; + ptr_allocator.deallocate(this->buffers_, 0); + this->buffers_ = nullptr; + } + this->buffer_count_ = 0; + this->buffer_size_ = 0; + this->total_length_ = 0; +} + +uint8_t &SplitBuffer::operator[](size_t index) { + if (index >= this->total_length_) { + ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); + // Return reference to a static dummy byte to avoid crash + static uint8_t dummy = 0; + return dummy; + } + + size_t buffer_index = index / this->buffer_size_; + size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; + + return this->buffers_[buffer_index][offset_in_buffer]; +} + +const uint8_t &SplitBuffer::operator[](size_t index) const { + if (index >= this->total_length_) { + ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); + // Return reference to a static dummy byte to avoid crash + static const uint8_t DUMMY = 0; + return DUMMY; + } + + size_t buffer_index = index / this->buffer_size_; + size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; + + return this->buffers_[buffer_index][offset_in_buffer]; +} + +} // namespace esphome::split_buffer diff --git a/esphome/components/split_buffer/split_buffer.h b/esphome/components/split_buffer/split_buffer.h new file mode 100644 index 0000000000..c3490f3d6e --- /dev/null +++ b/esphome/components/split_buffer/split_buffer.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace esphome::split_buffer { + +class SplitBuffer { + public: + SplitBuffer() = default; + ~SplitBuffer(); + + // Initialize the buffer with the desired total length + bool init(size_t total_length); + + // Free all allocated buffers + void free(); + + // Access operators + uint8_t &operator[](size_t index); + const uint8_t &operator[](size_t index) const; + + // Get the total length + size_t size() const { return this->total_length_; } + + // Get buffer information + size_t get_buffer_count() const { return this->buffer_count_; } + size_t get_buffer_size() const { return this->buffer_size_; } + + // Check if successfully initialized + bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; } + + private: + uint8_t **buffers_{nullptr}; + size_t buffer_count_{0}; + size_t buffer_size_{0}; + size_t total_length_{0}; +}; + +} // namespace esphome::split_buffer diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index b99bf416d6..21a782e49a 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -52,17 +52,19 @@ void SPS30Component::setup() { } else { result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); } - if (result) { - delay(20); - uint16_t secs[2]; - if (this->read_data(secs, 2)) { - this->fan_interval_ = secs[0] << 16 | secs[1]; - } - } - this->status_clear_warning(); - this->skipped_data_read_cycles_ = 0; - this->start_continuous_measurement_(); + this->set_timeout(20, [this, result]() { + if (result) { + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + this->fan_interval_ = secs[0] << 16 | secs[1]; + } + } + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + this->start_continuous_measurement_(); + this->setup_complete_ = true; + }); }); } @@ -111,6 +113,8 @@ void SPS30Component::dump_config() { } void SPS30Component::update() { + if (!this->setup_complete_) + return; /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { ESP_LOGD(TAG, "Reconnecting"); diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 461a770ab6..18847e16d9 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -30,9 +30,11 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri bool start_fan_cleaning(); protected: + bool setup_complete_{false}; uint16_t raw_firmware_version_; char serial_number_[17] = {0}; /// Terminating NULL character uint8_t skipped_data_read_cycles_ = 0; + bool start_continuous_measurement_(); enum ErrorCode : uint8_t { diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 710e473b11..14c21d5148 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -50,8 +50,10 @@ void HOT I2CST7567::write_display_data() { static const size_t BLOCK_SIZE = 64; for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { + size_t remaining = static_cast(this->get_width_internal()) - x; + size_t chunk = remaining > BLOCK_SIZE ? BLOCK_SIZE : remaining; this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], - this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x); + chunk); } } } diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 44f2293ac4..ade9c1126f 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -176,8 +176,9 @@ void ST7789V::write_display_data() { if (this->eightbitcolor_) { uint8_t temp_buffer[TEMP_BUFFER_SIZE]; size_t temp_index = 0; - for (int line = 0; line < this->get_buffer_length_(); line = line + this->get_width_internal()) { - for (int index = 0; index < this->get_width_internal(); ++index) { + size_t width = static_cast(this->get_width_internal()); + for (size_t line = 0; line < this->get_buffer_length_(); line += width) { + for (size_t index = 0; index < width; ++index) { auto color = display::ColorUtil::color_to_565( display::ColorUtil::to_color(this->buffer_[index + line], display::ColorOrder::COLOR_ORDER_RGB, display::ColorBitness::COLOR_BITNESS_332, true)); diff --git a/esphome/components/statsd/statsd.cpp b/esphome/components/statsd/statsd.cpp index 05f71c7b24..7729f36858 100644 --- a/esphome/components/statsd/statsd.cpp +++ b/esphome/components/statsd/statsd.cpp @@ -151,7 +151,7 @@ void StatsdComponent::send_(std::string *out) { int n_bytes = this->sock_->sendto(out->c_str(), out->length(), 0, reinterpret_cast(&this->destination_), sizeof(this->destination_)); - if (n_bytes != out->length()) { + if (n_bytes != static_cast(out->length())) { ESP_LOGE(TAG, "Failed to send UDP packed (%d of %d)", n_bytes, out->length()); } #endif diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index be412d62a8..ef93964a28 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() { // 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. optional Tormatic::read_gate_status_() { - if (this->available() < sizeof(MessageHeader)) { + if (this->available() < static_cast(sizeof(MessageHeader))) { return {}; } diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 07b0ff2815..91ddbc77ec 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -50,7 +50,7 @@ void TuyaSelect::dump_config() { " Options are:", this->select_id_, this->is_int_ ? "int" : "enum"); auto options = this->traits.get_options(); - for (auto i = 0; i < this->mappings_.size(); i++) { + for (size_t i = 0; i < this->mappings_.size(); i++) { ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); } } diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index d7e672d8cf..19a9112c73 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -58,7 +58,7 @@ void UponorSmatrixClimate::control(const climate::ClimateCall &call) { } void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) { - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { switch (data[i].id) { case UPONOR_ID_TARGET_TEMP_MIN: this->min_temperature_ = raw_to_celsius(data[i].value); diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp index 452660dc14..a1d0db214f 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -18,7 +18,7 @@ void UponorSmatrixSensor::dump_config() { } void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) { - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { switch (data[i].id) { case UPONOR_ID_ROOM_TEMP: if (this->temperature_sensor_ != nullptr) diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index a0017518bf..867305059f 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -122,7 +122,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // Decode packet payload data for easy access UponorSmatrixData data[data_len]; - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { data[i].id = packet[(i * 3) + 4]; 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. bool found_temperature = false; bool found_time = false; - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { if (data[i].id == UPONOR_ID_ROOM_TEMP) found_temperature = true; 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 >> 0); - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { packet.push_back(data[i].id); packet.push_back(data[i].value >> 8); packet.push_back(data[i].value >> 0); diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index c3b601e288..eb286ba21b 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -1,6 +1,7 @@ #include "veml7700.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include namespace esphome { namespace veml7700 { @@ -12,30 +13,30 @@ static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; } template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 75c6b84b79..3510d157d6 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2274,11 +2274,11 @@ void GDEW0154M09::clear_() { uint32_t pixsize = this->get_buffer_length_(); for (uint8_t j = 0; j < 2; j++) { this->command(CMD_DTM1_DATA_START_TRANS); - for (int count = 0; count < pixsize; count++) { + for (uint32_t count = 0; count < pixsize; count++) { this->data(0x00); } this->command(CMD_DTM2_DATA_START_TRANS2); - for (int count = 0; count < pixsize; count++) { + for (uint32_t count = 0; count < pixsize; count++) { this->data(0xff); } this->command(CMD_DISPLAY_REFRESH); @@ -2291,11 +2291,11 @@ void HOT GDEW0154M09::display() { this->init_internal_(); // "Mode 0 display" for now this->command(CMD_DTM1_DATA_START_TRANS); - for (int i = 0; i < this->get_buffer_length_(); i++) { + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { this->data(0xff); } this->command(CMD_DTM2_DATA_START_TRANS2); // write 'new' data to SRAM - for (int i = 0; i < this->get_buffer_length_(); i++) { + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { this->data(this->buffer_[i]); } this->command(CMD_DISPLAY_REFRESH); diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index fb02821760..6b27545549 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -9,83 +9,64 @@ namespace esphome { namespace web_server { -#ifdef USE_ARDUINO +#ifdef USE_ESP32 +ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} +#elif USE_ARDUINO ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) : web_server_(ws), events_(es) {} #endif -#ifdef USE_ESP_IDF -ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} -#endif ListEntitiesIterator::~ListEntitiesIterator() {} #ifdef USE_BINARY_SENSOR bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator); return true; } #endif #ifdef USE_COVER bool ListEntitiesIterator::on_cover(cover::Cover *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::cover_all_json_generator); return true; } #endif #ifdef USE_FAN bool ListEntitiesIterator::on_fan(fan::Fan *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::fan_all_json_generator); return true; } #endif #ifdef USE_LIGHT bool ListEntitiesIterator::on_light(light::LightState *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::light_all_json_generator); return true; } #endif #ifdef USE_SENSOR bool ListEntitiesIterator::on_sensor(sensor::Sensor *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::sensor_all_json_generator); return true; } #endif #ifdef USE_SWITCH bool ListEntitiesIterator::on_switch(switch_::Switch *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::switch_all_json_generator); return true; } #endif #ifdef USE_BUTTON bool ListEntitiesIterator::on_button(button::Button *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::button_all_json_generator); return true; } #endif #ifdef USE_TEXT_SENSOR bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_sensor_all_json_generator); return true; } #endif #ifdef USE_LOCK bool ListEntitiesIterator::on_lock(lock::Lock *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::lock_all_json_generator); return true; } @@ -93,8 +74,6 @@ bool ListEntitiesIterator::on_lock(lock::Lock *obj) { #ifdef USE_VALVE bool ListEntitiesIterator::on_valve(valve::Valve *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::valve_all_json_generator); return true; } @@ -102,8 +81,6 @@ bool ListEntitiesIterator::on_valve(valve::Valve *obj) { #ifdef USE_CLIMATE bool ListEntitiesIterator::on_climate(climate::Climate *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::climate_all_json_generator); return true; } @@ -111,8 +88,6 @@ bool ListEntitiesIterator::on_climate(climate::Climate *obj) { #ifdef USE_NUMBER bool ListEntitiesIterator::on_number(number::Number *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::number_all_json_generator); return true; } @@ -120,8 +95,6 @@ bool ListEntitiesIterator::on_number(number::Number *obj) { #ifdef USE_DATETIME_DATE bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::date_all_json_generator); return true; } @@ -129,8 +102,6 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) { #ifdef USE_DATETIME_TIME bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::time_all_json_generator); return true; } @@ -138,8 +109,6 @@ bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) { #ifdef USE_DATETIME_DATETIME bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::datetime_all_json_generator); return true; } @@ -147,8 +116,6 @@ bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) { #ifdef USE_TEXT bool ListEntitiesIterator::on_text(text::Text *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_all_json_generator); return true; } @@ -156,8 +123,6 @@ bool ListEntitiesIterator::on_text(text::Text *obj) { #ifdef USE_SELECT bool ListEntitiesIterator::on_select(select::Select *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::select_all_json_generator); return true; } @@ -165,8 +130,6 @@ bool ListEntitiesIterator::on_select(select::Select *obj) { #ifdef USE_ALARM_CONTROL_PANEL bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::alarm_control_panel_all_json_generator); return true; } @@ -174,8 +137,6 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont #ifdef USE_EVENT bool ListEntitiesIterator::on_event(event::Event *obj) { - if (this->events_->count() == 0) - return true; // Null event type, since we are just iterating over entities this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::event_all_json_generator); return true; @@ -184,8 +145,6 @@ bool ListEntitiesIterator::on_event(event::Event *obj) { #ifdef USE_UPDATE bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::update_all_json_generator); return true; } diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index ba81c70c86..43e1cc2544 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -5,25 +5,24 @@ #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" namespace esphome { -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 namespace web_server_idf { class AsyncEventSource; } #endif namespace web_server { -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) class DeferredUpdateEventSource; #endif class WebServer; class ListEntitiesIterator : public ComponentIterator { public: -#ifdef USE_ARDUINO - ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); +#elif defined(USE_ARDUINO) + ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); #endif virtual ~ListEntitiesIterator(); #ifdef USE_BINARY_SENSOR @@ -90,11 +89,10 @@ class ListEntitiesIterator : public ComponentIterator { protected: const WebServer *web_server_; -#ifdef USE_ARDUINO - DeferredUpdateEventSource *events_; -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 esphome::web_server_idf::AsyncEventSource *events_; +#elif USE_ARDUINO + DeferredUpdateEventSource *events_; #endif }; diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 22e56639e1..4a98db8877 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -29,5 +29,5 @@ async def to_code(config): await ota_to_code(var, config) await cg.register_component(var, config) cg.add_define("USE_WEBSERVER_OTA") - if CORE.using_esp_idf: + if CORE.is_esp32: add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 672a9868c5..7929f3647f 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -17,6 +17,12 @@ #endif #endif // USE_ARDUINO +#if USE_ESP32 +using PlatformString = std::string; +#elif USE_ARDUINO +using PlatformString = String; +#endif + namespace esphome { namespace web_server { @@ -26,8 +32,8 @@ class OTARequestHandler : public AsyncWebHandler { public: OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {} void handleRequest(AsyncWebServerRequest *request) override; - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override; + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { // Check if this is an OTA update request bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; @@ -100,7 +106,7 @@ void OTARequestHandler::ota_init_(const char *filename) { this->ota_success_ = false; } -void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, +void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, size_t len, bool final) { ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 33141c2049..f18f21b16b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -8,7 +8,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) #include "StreamString.h" #endif @@ -103,7 +103,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) return match; } -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); @@ -127,6 +127,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() { deferred_queue_.erase(deferred_queue_.begin()); this->consecutive_send_failures_ = 0; // Reset failure count on successful send } else { + // NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_() + // The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs + // fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic, + // also update the ESP-IDF implementation. this->consecutive_send_failures_++; if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { // Too many failures, connection is likely dead @@ -148,6 +152,10 @@ void DeferredUpdateEventSource::loop() { void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { + // Skip if no connected clients to avoid unnecessary deferred queue processing + if (this->count() == 0) + return; + // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing // up in the web GUI and reduces event load during initial connect if (!entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all")) @@ -193,6 +201,9 @@ void DeferredUpdateEventSourceList::loop() { void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { + // Skip if no event sources (no connected clients) to avoid unnecessary iteration + if (this->empty()) + return; for (DeferredUpdateEventSource *dues : *this) { dues->deferrable_send_state(source, event_type, message_generator); } @@ -297,7 +308,7 @@ void WebServer::setup() { } #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 this->base_->add_handler(&this->events_); #endif this->base_->add_handler(this); @@ -381,11 +392,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #endif // Helper functions to reduce code size by avoiding macro expansion -static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { - root["id"] = id; +static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { + char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null + const auto &object_id = obj->get_object_id(); + snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); + root["id"] = id_buf; if (start_config == DETAIL_ALL) { root["name"] = obj->get_name(); - root["icon"] = obj->get_icon(); + root["icon"] = obj->get_icon_ref(); root["entity_category"] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) @@ -393,17 +407,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id } } +// Keep as separate function even though only used once: reduces code size by ~48 bytes +// by allowing compiler to share code between template instantiations (bool, float, etc.) template -static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, +static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, JsonDetail start_config) { - set_json_id(root, obj, id, start_config); + set_json_id(root, obj, prefix, start_config); root["value"] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, - const std::string &state, const T &value, JsonDetail start_config) { - set_json_value(root, obj, id, value, start_config); +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, + const T &value, JsonDetail start_config) { + set_json_value(root, obj, prefix, value, start_config); root["state"] = state; } @@ -415,8 +431,6 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) { #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -442,20 +456,15 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - // Build JSON directly inline - std::string state; - if (std::isnan(value)) { - state = "NA"; - } else { - state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - if (!obj->get_unit_of_measurement().empty()) - state += " " + obj->get_unit_of_measurement(); - } - set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); + const auto uom_ref = obj->get_unit_of_measurement_ref(); + + std::string state = + std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); + set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); - if (!obj->get_unit_of_measurement().empty()) - root["uom"] = obj->get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; } return builder.serialize(); @@ -464,8 +473,6 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail #ifdef USE_TEXT_SENSOR void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -494,7 +501,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -505,8 +512,6 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: #ifdef USE_SWITCH void WebServer::on_switch_update(switch_::Switch *obj, bool state) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", switch_state_json_generator); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -567,7 +572,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); this->add_sorting_info_(root, obj); @@ -607,7 +612,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "button", start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -618,8 +623,6 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -647,8 +650,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, - start_config); + set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -659,8 +661,6 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool #ifdef USE_FAN void WebServer::on_fan_update(fan::Fan *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", fan_state_json_generator); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -717,8 +717,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, - start_config); + set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; @@ -736,8 +735,6 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", light_state_json_generator); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -793,8 +790,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); - root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; + set_json_value(root, obj, "light", obj->remote_values.is_on() ? "ON" : "OFF", start_config); light::LightJSONSchema::dump_json(*obj, root); if (start_config == DETAIL_ALL) { @@ -812,8 +808,6 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", cover_state_json_generator); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -881,8 +875,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -899,8 +893,6 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { #ifdef USE_NUMBER void WebServer::on_number_update(number::Number *obj, float state) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", number_state_json_generator); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -939,7 +931,15 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); + const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + + std::string val_str = std::isnan(value) + ? "\"NaN\"" + : value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); + std::string state_str = std::isnan(value) ? "NA" + : value_accuracy_with_uom_to_string( + value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); + set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); if (start_config == DETAIL_ALL) { root["min_value"] = value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); @@ -947,20 +947,10 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); root["mode"] = (int) obj->traits.get_mode(); - if (!obj->traits.get_unit_of_measurement().empty()) - root["uom"] = obj->traits.get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; this->add_sorting_info_(root, obj); } - if (std::isnan(value)) { - root["value"] = "\"NaN\""; - root["state"] = "NA"; - } else { - root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - if (!obj->traits.get_unit_of_measurement().empty()) - state += " " + obj->traits.get_unit_of_measurement(); - root["state"] = state; - } return builder.serialize(); } @@ -968,8 +958,6 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #ifdef USE_DATETIME_DATE void WebServer::on_date_update(datetime::DateEntity *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", date_state_json_generator); } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1013,10 +1001,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); - root["value"] = value; - root["state"] = value; + set_json_icon_state_value(root, obj, "date", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1027,8 +1013,6 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", time_state_json_generator); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1071,10 +1055,8 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); - root["value"] = value; - root["state"] = value; + set_json_icon_state_value(root, obj, "time", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1085,8 +1067,6 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_DATETIME void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator); } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1129,11 +1109,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); - root["value"] = value; - root["state"] = value; + set_json_icon_state_value(root, obj, "datetime", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1144,8 +1122,6 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s #ifdef USE_TEXT void WebServer::on_text_update(text::Text *obj, const std::string &state) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", text_state_json_generator); } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1184,16 +1160,11 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); + std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; + set_json_icon_state_value(root, obj, "text", state, value, start_config); root["min_length"] = obj->traits.get_min_length(); root["max_length"] = obj->traits.get_max_length(); root["pattern"] = obj->traits.get_pattern(); - if (obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD) { - root["state"] = "********"; - } else { - root["state"] = value; - } - root["value"] = value; if (start_config == DETAIL_ALL) { root["mode"] = (int) obj->traits.get_mode(); this->add_sorting_info_(root, obj); @@ -1205,8 +1176,6 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json #ifdef USE_SELECT void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", select_state_json_generator); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1245,7 +1214,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "select", value, value, start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root["option"].to(); for (auto &option : obj->traits.get_options()) { @@ -1259,12 +1228,10 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value #endif // Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15) +#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15) #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", climate_state_json_generator); } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1314,7 +1281,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "climate", start_config); const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -1405,8 +1372,6 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", lock_state_json_generator); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1467,8 +1432,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, - start_config); + set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1479,8 +1443,6 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet #ifdef USE_VALVE void WebServer::on_valve_update(valve::Valve *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", valve_state_json_generator); } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1546,8 +1508,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -1562,8 +1524,6 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { #ifdef USE_ALARM_CONTROL_PANEL void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator); } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1630,8 +1590,8 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro JsonObject root = builder.root(); char buf[16]; - set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), - PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); + set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), + value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1676,7 +1636,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "event", start_config); if (!event_type.empty()) { root["event_type"] = event_type; } @@ -1685,7 +1645,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty for (auto const &event_type : obj->get_event_types()) { event_types.add(event_type); } - root["device_class"] = obj->get_device_class(); + root["device_class"] = obj->get_device_class_ref(); this->add_sorting_info_(root, obj); } @@ -1708,8 +1668,6 @@ static const char *update_state_to_string(update::UpdateState state) { } void WebServer::on_update(update::UpdateEntity *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", update_state_json_generator); } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1748,9 +1706,8 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); - root["value"] = obj->update_info.latest_version; - root["state"] = update_state_to_string(obj->state); + set_json_icon_state_value(root, obj, "update", update_state_to_string(obj->state), obj->update_info.latest_version, + start_config); if (start_config == DETAIL_ALL) { root["current_version"] = obj->update_info.current_version; root["title"] = obj->update_info.title; @@ -1770,15 +1727,15 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { // Static URL checks static const char *const STATIC_URLS[] = { - "/", -#ifdef USE_ARDUINO - "/events", + "/", +#if !defined(USE_ESP32) && defined(USE_ARDUINO) + "/events", #endif #ifdef USE_WEBSERVER_CSS_INCLUDE - "/0.css", + "/0.css", #endif #ifdef USE_WEBSERVER_JS_INCLUDE - "/0.js", + "/0.js", #endif }; @@ -1899,7 +1856,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) if (url == "/events") { this->events_.add_new_client(this, request); return; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index e42c35b32d..2e5d58d375 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -81,7 +81,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround can be forgotten. */ -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) using message_generator_t = std::string(WebServer *, void *); class DeferredUpdateEventSourceList; @@ -164,7 +164,7 @@ class DeferredUpdateEventSourceList : public std::listjs_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); const std::string &title = App.get_name(); - stream->print(F("")); + stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " + "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); stream->print(title.c_str()); - stream->print(F("")); + stream->print(ESPHOME_F("")); #ifdef USE_WEBSERVER_CSS_INCLUDE - stream->print(F("")); + stream->print(ESPHOME_F("")); #endif if (strlen(this->css_url_) > 0) { - stream->print(F(R"(print(ESPHOME_F(R"(print(this->css_url_); - stream->print(F("\">")); + stream->print(ESPHOME_F("\">")); } - stream->print(F("")); - stream->print(F("

")); + stream->print(ESPHOME_F("")); + stream->print(ESPHOME_F("

")); stream->print(title.c_str()); - stream->print(F("

")); - stream->print(F("

States

")); + stream->print(ESPHOME_F("")); + stream->print(ESPHOME_F("

States

NameStateActions
")); #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) { @@ -190,26 +190,28 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif - stream->print(F("
NameStateActions

See ESPHome Web API for " - "REST API documentation.

")); + stream->print( + ESPHOME_F("

See ESPHome Web API for " + "REST API documentation.

")); #if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED) // Show OTA form only if web_server OTA is not explicitly disabled // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal - stream->print(F("

OTA Update

")); + stream->print( + ESPHOME_F("

OTA Update

")); #endif - stream->print(F("

Debug Log

"));
+  stream->print(ESPHOME_F("

Debug Log

"));
 #ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
-    stream->print(F(""));
+    stream->print(ESPHOME_F(""));
   }
 #endif
   if (strlen(this->js_url_) > 0) {
-    stream->print(F(""));
+    stream->print(ESPHOME_F("\">"));
   }
-  stream->print(F("
")); + stream->print(ESPHOME_F("

")); request->send(stream); } diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index a82ec462d9..4cf76eba0e 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -9,10 +9,10 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): + if CORE.is_esp32: + return ["web_server_idf"] if CORE.using_arduino: return ["async_tcp"] - if CORE.using_esp_idf: - return ["web_server_idf"] return [] @@ -33,6 +33,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) + if CORE.is_esp32: + return + if CORE.using_arduino: if CORE.is_esp32: cg.add_library("WiFi", None) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index cfca776ee1..039a452d64 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -7,11 +7,31 @@ #include "esphome/core/component.h" -#ifdef USE_ARDUINO -#include -#elif USE_ESP_IDF +// Platform-agnostic macros for web server components +// On ESP32 (both Arduino and IDF): Use plain strings (no PROGMEM) +// On ESP8266: Use Arduino's F() macro for PROGMEM strings +#ifdef USE_ESP32 +#define ESPHOME_F(string_literal) (string_literal) +#define ESPHOME_PGM_P const char * +#define ESPHOME_strncpy_P strncpy +#else +// ESP8266 uses Arduino macros +#define ESPHOME_F(string_literal) F(string_literal) +#define ESPHOME_PGM_P PGM_P +#define ESPHOME_strncpy_P strncpy_P +#endif + +#if USE_ESP32 #include "esphome/core/hal.h" #include "esphome/components/web_server_idf/web_server_idf.h" +#else +#include +#endif + +#if USE_ESP32 +using PlatformString = std::string; +#elif USE_ARDUINO +using PlatformString = String; #endif namespace esphome { @@ -28,8 +48,8 @@ class MiddlewareHandler : public AsyncWebHandler { bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); } void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override { + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override { next_->handleUpload(request, filename, index, data, len, final); } void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { @@ -65,8 +85,8 @@ class AuthMiddlewareHandler : public MiddlewareHandler { return; MiddlewareHandler::handleRequest(request); } - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override { + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override { if (!check_auth(request)) return; MiddlewareHandler::handleUpload(request, filename, index, data, len, final); diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 506e1c5c13..74a9d657a6 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -5,7 +5,7 @@ CODEOWNERS = ["@dentra"] CONFIG_SCHEMA = cv.All( cv.Schema({}), - cv.only_with_esp_idf, + cv.only_on_esp32, ) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 8655226ab9..2092a41a8e 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -1,5 +1,5 @@ #include "esphome/core/defines.h" -#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) #include "multipart.h" #include "utils.h" #include "esphome/core/log.h" @@ -251,4 +251,4 @@ std::string str_trim(const std::string &str) { } // namespace web_server_idf } // namespace esphome -#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 967c72ffa5..8fbe90c4a0 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -1,6 +1,6 @@ #pragma once #include "esphome/core/defines.h" -#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) #include #include @@ -83,4 +83,4 @@ std::string str_trim(const std::string &str); } // namespace web_server_idf } // namespace esphome -#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index ac5df90bb8..d5d34b520b 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include #include @@ -122,4 +122,4 @@ const char *stristr(const char *haystack, const char *needle) { } // namespace web_server_idf } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 988b962d72..f70a5f0760 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -24,4 +24,4 @@ const char *stristr(const char *haystack, const char *needle); } // namespace web_server_idf } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 51d763c508..d90efd18bc 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -25,6 +25,10 @@ #include "esphome/components/web_server/list_entities.h" #endif // USE_WEBSERVER +// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts +#include +#include + namespace esphome { namespace web_server_idf { @@ -46,6 +50,42 @@ DefaultHeaders default_headers_instance; DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; } +namespace { +// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full +/** + * Sends data on a socket in non-blocking mode. + * + * @param hd HTTP server handle (unused). + * @param sockfd Socket file descriptor. + * @param buf Buffer to send. + * @param buf_len Length of buffer. + * @param flags Flags for send(). + * @return + * - Number of bytes sent on success. + * - HTTPD_SOCK_ERR_INVALID if buf is nullptr. + * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). + * - HTTPD_SOCK_ERR_FAIL for other errors. + */ +int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { + if (buf == nullptr) { + return HTTPD_SOCK_ERR_INVALID; + } + + // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full + int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Buffer full - retry later + return HTTPD_SOCK_ERR_TIMEOUT; + } + // Real error + ESP_LOGD(TAG, "send error: errno %d", errno); + return HTTPD_SOCK_ERR_FAIL; + } + return ret; +} +} // namespace + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -164,8 +204,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const AsyncWebServerRequest::~AsyncWebServerRequest() { delete this->rsp_; - for (const auto &pair : this->params_) { - delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + for (auto *param : this->params_) { + delete param; // NOLINT(cppcoreguidelines-owning-memory) } } @@ -205,10 +245,22 @@ void AsyncWebServerRequest::redirect(const std::string &url) { } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { - httpd_resp_set_status(*this, code == 200 ? HTTPD_200 - : code == 404 ? HTTPD_404 - : code == 409 ? HTTPD_409 - : to_string(code).c_str()); + // Set status code - use constants for common codes to avoid string allocation + const char *status = nullptr; + switch (code) { + case 200: + status = HTTPD_200; + break; + case 404: + status = HTTPD_404; + break; + case 409: + status = HTTPD_409; + break; + default: + break; + } + httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); @@ -265,11 +317,14 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { #endif AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { - auto find = this->params_.find(name); - if (find != this->params_.end()) { - return find->second; + // Check cache first - only successful lookups are cached + for (auto *param : this->params_) { + if (param->name() == name) { + return param; + } } + // Look up value from query strings optional val = query_key_value(this->post_query_, name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); @@ -278,11 +333,14 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } } - AsyncWebParameter *param = nullptr; - if (val.has_value()) { - param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) + // Don't cache misses to avoid wasting memory when handlers check for + // optional parameters that don't exist in the request + if (!val.has_value()) { + return nullptr; } - this->params_.insert({name, param}); + + auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory) + this->params_.push_back(param); return param; } @@ -354,6 +412,9 @@ void AsyncEventSource::try_send_nodefer(const char *message, const char *event, void AsyncEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { + // Skip if no connected clients to avoid unnecessary processing + if (this->empty()) + return; for (auto *ses : this->sessions_) { if (ses->fd_.load() != 0) { // Skip dead sessions ses->deferrable_send_state(source, event_type, message_generator); @@ -384,6 +445,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * this->hd_ = req->handle; this->fd_.store(httpd_req_to_sockfd(req)); + // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full + httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send); + // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect std::string message = ws->get_config_json(); @@ -459,15 +523,45 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, - event_buffer_.size() - event_bytes_sent_, 0); - if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { - // Socket error - just return, the connection will be closed by httpd - // and our destroy callback will be called + size_t remaining = event_buffer_.size() - event_bytes_sent_; + int bytes_sent = + httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0); + if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) { + // EAGAIN/EWOULDBLOCK - socket buffer full, try again later + // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_() + // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs + // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also + // update the Arduino implementation. + this->consecutive_send_failures_++; + if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { + // Too many failures, connection is likely dead + ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends", + this->consecutive_send_failures_); + this->fd_.store(0); // Mark for cleanup + this->deferred_queue_.clear(); + } return; } + if (bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Real socket error - connection will be closed by httpd and destroy callback will be called + return; + } + if (bytes_sent <= 0) { + // Unexpected error or zero bytes sent + ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent); + return; + } + + // Successful send - reset failure counter + this->consecutive_send_failures_ = 0; event_bytes_sent_ += bytes_sent; + // Log partial sends for debugging + if (event_bytes_sent_ < event_buffer_.size()) { + ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_, + event_buffer_.size()); + } + if (event_bytes_sent_ == event_buffer_.size()) { event_buffer_.resize(0); event_bytes_sent_ = 0; @@ -670,4 +764,4 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } // namespace web_server_idf } // namespace esphome -#endif // !defined(USE_ESP_IDF) +#endif // !defined(USE_ESP32) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 76540ef232..bf93dcbd34 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include @@ -22,18 +22,14 @@ class ListEntitiesIterator; #endif namespace web_server_idf { -#define F(string_literal) (string_literal) -#define PGM_P const char * -#define strncpy_P strncpy - -using String = std::string; - class AsyncWebParameter { public: - AsyncWebParameter(std::string value) : value_(std::move(value)) {} + AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {} + const std::string &name() const { return this->name_; } const std::string &value() const { return this->value_; } protected: + std::string name_; std::string value_; }; @@ -174,7 +170,11 @@ class AsyncWebServerRequest { protected: httpd_req_t *req_; AsyncWebServerResponse *rsp_{}; - std::map params_; + // Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search + // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid + // duplicate storage. Only successful lookups are cached to prevent cache pollution when + // handlers check for optional parameters that don't exist. + std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} @@ -283,6 +283,8 @@ class AsyncEventSourceResponse { std::unique_ptr entities_iterator_; std::string event_buffer_{""}; size_t event_bytes_sent_; + uint16_t consecutive_send_failures_{0}; + static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate }; using AsyncEventSourceClient = AsyncEventSourceResponse; @@ -341,4 +343,4 @@ class DefaultHeaders { using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers) -#endif // !defined(USE_ESP_IDF) +#endif // !defined(USE_ESP32) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8c7b55c274..2e083d4c68 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,7 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI #include -#include #ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) @@ -42,6 +41,25 @@ namespace wifi { static const char *const TAG = "wifi"; +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { + switch (type) { + case ESP_EAP_TTLS_PHASE2_PAP: + return "pap"; + case ESP_EAP_TTLS_PHASE2_CHAP: + return "chap"; + case ESP_EAP_TTLS_PHASE2_MSCHAP: + return "mschap"; + case ESP_EAP_TTLS_PHASE2_MSCHAPV2: + return "mschapv2"; + case ESP_EAP_TTLS_PHASE2_EAP: + return "eap"; + default: + return "unknown"; + } +} +#endif + float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { @@ -266,30 +284,34 @@ void WiFiComponent::setup_ap_config_() { std::string name = App.get_name(); if (name.length() > 32) { if (App.is_name_add_mac_suffix_enabled()) { - name.erase(name.begin() + 25, name.end() - 7); // Remove characters between 25 and the mac address + // Keep first 25 chars and last 7 chars (MAC suffix), remove middle + name.erase(25, name.length() - 32); } else { - name = name.substr(0, 32); + name.resize(32); } } this->ap_.set_ssid(name); } + this->ap_setup_ = this->wifi_start_ap_(this->ap_); + + auto ip_address = this->wifi_soft_ap_ip().str(); ESP_LOGCONFIG(TAG, "Setting up AP:\n" " AP SSID: '%s'\n" - " AP Password: '%s'", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str()); - if (this->ap_.get_manual_ip().has_value()) { - auto manual = *this->ap_.get_manual_ip(); + " AP Password: '%s'\n" + " IP Address: %s", + this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); + + auto manual_ip = this->ap_.get_manual_ip(); + if (manual_ip.has_value()) { ESP_LOGCONFIG(TAG, " AP Static IP: '%s'\n" " AP Gateway: '%s'\n" " AP Subnet: '%s'", - manual.static_ip.str().c_str(), manual.gateway.str().c_str(), manual.subnet.str().c_str()); + manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), + manual_ip->subnet.str().c_str()); } - this->ap_setup_ = this->wifi_start_ap_(this->ap_); - ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().str().c_str()); - if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; } @@ -312,9 +334,9 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { } void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { - SavedWifiSettings save{}; - snprintf(save.ssid, sizeof(save.ssid), "%s", ssid.c_str()); - snprintf(save.password, sizeof(save.password), "%s", password.c_str()); + SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination + strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 + strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); @@ -331,8 +353,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - bssid_t b = *ap.get_bssid(); - ESP_LOGV(TAG, " BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]); + ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str()); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } @@ -344,15 +365,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); -#ifdef USE_ESP32 -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - std::map phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, - {ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"}, - {ESP_EAP_TTLS_PHASE2_EAP, "eap"}}; - ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str()); -#endif +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2)); #endif bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); @@ -446,7 +460,6 @@ void WiFiComponent::print_connect_params_() { ESP_LOGCONFIG(TAG, " Disabled"); return; } - ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); @@ -454,24 +467,23 @@ void WiFiComponent::print_connect_params_() { } int8_t rssi = wifi_rssi(); ESP_LOGCONFIG(TAG, - " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X") "\n" - " Hostname: '%s'\n" - " Signal strength: %d dB %s", - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi))); + " SSID: " LOG_SECRET("'%s'") "\n" + " BSSID: " LOG_SECRET("%s") "\n" + " Hostname: '%s'\n" + " Signal strength: %d dB %s\n" + " Channel: %" PRId32 "\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s", + wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, + LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), + wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (this->selected_ap_.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); } #endif - ESP_LOGCONFIG(TAG, - " Channel: %" PRId32 "\n" - " Subnet: %s\n" - " Gateway: %s\n" - " DNS1: %s\n" - " DNS2: %s", - get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), - wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef USE_WIFI_11KV_SUPPORT ESP_LOGCONFIG(TAG, " BTM: %s\n" @@ -557,6 +569,25 @@ static void insertion_sort_scan_results(std::vector &results) { } } +// Helper function to log scan results - marked noinline to prevent re-inlining into loop +__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) { + char bssid_s[18]; + auto bssid = res.get_bssid(); + format_mac_addr_upper(bssid.data(), bssid_s); + + if (res.get_matches()) { + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "", + bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + ESP_LOGD(TAG, + " Channel: %u\n" + " RSSI: %d dB", + res.get_channel(), res.get_rssi()); + } else { + ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + } +} + void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { @@ -591,21 +622,7 @@ void WiFiComponent::check_scanning_finished() { insertion_sort_scan_results(this->scan_result_); for (auto &res : this->scan_result_) { - char bssid_s[18]; - auto bssid = res.get_bssid(); - format_mac_addr_upper(bssid.data(), bssid_s); - - if (res.get_matches()) { - ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), - res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, - " Channel: %u\n" - " RSSI: %d dB", - res.get_channel(), res.get_rssi()); - } else { - ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, - LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - } + log_scan_result(res); } if (!this->scan_result_[0].get_matches()) { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ae1daed8b5..3b3b4b139c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -301,7 +301,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // if we have certs, this must be EAP-TLS ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + (uint8_t *) eap.password.c_str(), eap.password.length()); if (ret) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret); } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2d1eba8885..ccec800205 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -408,11 +408,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + (uint8_t *) eap.password.c_str(), eap.password.length()); #else err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + (uint8_t *) eap.password.c_str(), eap.password.length()); #endif if (err != ESP_OK) { ESP_LOGV(TAG, "set_cert_key failed %d", err); diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 70932da87c..a26a9b2335 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -101,15 +101,7 @@ void ZWaveProxy::process_uart_() { // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed // The frame parser has already validated the checksum and ensured all bytes are present if (this->set_home_id(&this->buffer_[4])) { - api::ZWaveProxyRequest msg; - msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; - msg.data = this->home_id_.data(); - msg.data_len = this->home_id_.size(); - if (api::global_api_server != nullptr) { - // We could add code to manage a second subscription type, but, since this message is - // very infrequent and small, we simply send it to all clients - api::global_api_server->on_zwave_proxy_request(msg); - } + this->send_homeid_changed_msg_(); } } ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); @@ -135,6 +127,13 @@ void ZWaveProxy::dump_config() { format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); } +void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { + if (this->home_id_ready_) { + // If a client just authenticated & HomeID is ready, send the current HomeID + this->send_homeid_changed_msg_(conn); + } +} + void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { switch (type) { case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: @@ -178,6 +177,21 @@ void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { this->write_array(data, length); } +void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) { + api::ZWaveProxyRequest msg; + msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + msg.data = this->home_id_.data(); + msg.data_len = this->home_id_.size(); + if (conn != nullptr) { + // Send to specific connection + conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + } else if (api::global_api_server != nullptr) { + // We could add code to manage a second subscription type, but, since this message is + // very infrequent and small, we simply send it to all clients + api::global_api_server->on_zwave_proxy_request(msg); + } +} + void ZWaveProxy::send_simple_command_(const uint8_t command_id) { // Send a simple Z-Wave command with no parameters // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM] diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index a9123a81ca..20d9090d98 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -49,6 +49,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { float get_setup_priority() const override; bool can_proceed() override; + void api_connection_authenticated(api::APIConnection *conn); void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type); api::APIConnection *get_api_connection() { return this->api_connection_; } @@ -61,6 +62,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { void send_frame(const uint8_t *data, size_t length); protected: + void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) void parse_start_(uint8_t byte); diff --git a/esphome/config.py b/esphome/config.py index a5297a53cb..10a5733575 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -67,6 +67,31 @@ ConfigPath = list[str | int] 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( result: Config, component_name: str, @@ -91,9 +116,7 @@ def _process_platform_config( CORE.loaded_platforms.add(f"{component_name}/{platform_name}") # Process platform's AUTO_LOAD - for load in platform.auto_load: - if load not in result: - result.add_validation_step(AutoLoadValidationStep(load)) + _process_auto_load(result, platform, path) # Add validation steps for the platform p_domain = f"{component_name}.{platform_name}" @@ -390,9 +413,7 @@ class LoadValidationStep(ConfigValidationStep): result[self.domain] = self.conf = [self.conf] # Process AUTO_LOAD - for load in component.auto_load: - if load not in result: - result.add_validation_step(AutoLoadValidationStep(load)) + _process_auto_load(result, component, path) result.add_validation_step( MetadataValidationStep([self.domain], self.domain, self.conf, component) @@ -618,6 +639,34 @@ class MetadataValidationStep(ConfigValidationStep): 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 = -5.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): """Schema validation step. diff --git a/esphome/const.py b/esphome/const.py index ec583beeb6..086b5b4ce3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.10.0-dev" +__version__ = "2025.11.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -174,6 +174,7 @@ CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" CONF_CAPACITY = "capacity" +CONF_CAPTURE_RESPONSE = "capture_response" CONF_CARBON_MONOXIDE = "carbon_monoxide" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" @@ -542,6 +543,7 @@ CONF_MANUAL_IP = "manual_ip" CONF_MANUFACTURER_ID = "manufacturer_id" CONF_MASK_DISTURBER = "mask_disturber" CONF_MAX_BRIGHTNESS = "max_brightness" +CONF_MAX_CONNECTIONS = "max_connections" CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time" CONF_MAX_CURRENT = "max_current" CONF_MAX_DURATION = "max_duration" @@ -675,6 +677,7 @@ CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_STATE = "on_state" +CONF_ON_SUCCESS = "on_success" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TIME = "on_time" @@ -817,6 +820,7 @@ CONF_RESET_DURATION = "reset_duration" CONF_RESET_PIN = "reset_pin" CONF_RESIZE = "resize" CONF_RESOLUTION = "resolution" +CONF_RESPONSE_TEMPLATE = "response_template" CONF_RESTART = "restart" CONF_RESTORE = "restore" CONF_RESTORE_MODE = "restore_mode" @@ -1169,7 +1173,7 @@ UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" UNIT_KILOVOLT_AMPS = "kVA" UNIT_KILOVOLT_AMPS_HOURS = "kVAh" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE = "kvar" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 554e1ee13c..0f1d1bcf28 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -48,6 +48,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG #define USE_LVGL_ARC @@ -83,6 +84,7 @@ #define USE_LVGL_TOUCHSCREEN #define USE_MDNS #define MDNS_SERVICE_COUNT 3 +#define MDNS_DYNAMIC_TXT_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER @@ -111,6 +113,8 @@ #define USE_API #define USE_API_CLIENT_CONNECTED_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_STATES #define USE_API_NOISE @@ -158,6 +162,7 @@ #define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE +#define USE_ESP32_BLE_MAX_CONNECTIONS 3 #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_DEVICE #define USE_ESP32_BLE_SERVER diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index 4eb6a89f53..c45c4df70b 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -39,7 +39,7 @@ class HashBase { /// Compare the hash against a provided hex-encoded hash bool equals_hex(const char *expected) { - uint8_t parsed[this->get_size()]; + uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes) if (!parse_hex(expected, parsed, this->get_size())) { return false; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 662d0d29e9..a46f944385 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include #include @@ -348,17 +349,34 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { +static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { auto multiplier = powf(10.0f, accuracy_decimals); value = roundf(value * multiplier) / multiplier; accuracy_decimals = 0; } +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + normalize_accuracy_decimals(value, accuracy_decimals); char tmp[32]; // should be enough, but we should maybe improve this at some point. snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { + normalize_accuracy_decimals(value, accuracy_decimals); + // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") + // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short + char tmp[64]; + if (unit_of_measurement.empty()) { + snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); + } else { + snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + } + return std::string(tmp); +} + int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; @@ -611,8 +629,6 @@ bool mac_address_is_valid(const uint8_t *mac) { if (mac[i] != 0) { is_all_zeros = false; } - } - for (uint8_t i = 0; i < 6; i++) { if (mac[i] != 0xFF) { is_all_ones = false; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 39d39c1c94..b3e2ab79cf 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -45,6 +45,9 @@ namespace esphome { +// Forward declaration to avoid circular dependency with string_ref.h +class StringRef; + /// @name STL backports ///@{ @@ -127,6 +130,16 @@ template class StaticVector { } } + // Return reference to next element and increment count (with bounds checking) + T &emplace_next() { + if (count_ >= N) { + // Should never happen with proper size calculation + // Return reference to last element to avoid crash + return data_[N - 1]; + } + return data_[count_++]; + } + size_t size() const { return count_; } bool empty() const { return count_ == 0; } @@ -603,6 +616,8 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +/// Create a string from a value, an accuracy in decimals, and a unit of measurement. +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 71e2a00fbe..402084f306 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -118,7 +118,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->type = type; item->callback = std::move(func); // Initialize remove to false (though it should already be from constructor) - // Not using mark_item_removed_ helper since we're setting to false, not true #ifdef ESPHOME_THREAD_MULTI_ATOMICS item->remove.store(false, std::memory_order_relaxed); #else @@ -600,12 +599,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - } - } + total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -620,23 +614,13 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - this->to_remove_++; // Track removals for heap items - } - } + size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + total_cancelled += heap_cancelled; + this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - for (auto &item : this->to_add_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - // Don't track removals for to_add_ items - } - } + total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 885ee13754..2237915e07 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -280,19 +280,30 @@ class Scheduler { #endif } - // Helper to mark item for removal (platform-specific) + // Helper to mark matching items in a container as removed + // Returns the number of items marked for removal // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. - void mark_item_removed_(SchedulerItem *item) { + template + size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { + size_t count = 0; + for (auto &item : container) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + // Mark item for removal (platform-specific) #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic store - item->remove.store(true, std::memory_order_release); + // Multi-threaded with atomics: use atomic store + item->remove.store(true, std::memory_order_release); #else - // Single-threaded (ESPHOME_THREAD_SINGLE) or - // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write - // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! - item->remove = true; + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = true; #endif + count++; + } + } + return count; } // Template helper to check if any item in a container matches our criteria diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index fe6f50158c..1285ec6448 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -77,7 +77,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { &hour, // NOLINT &minute, // NOLINT &second, &num) == 6 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.year = year; esp_time.month = month; 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 &hour, // NOLINT &minute, &num) == 5 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.year = year; esp_time.month = month; 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.second = 0; } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.hour = hour; esp_time.minute = minute; esp_time.second = second; } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.hour = hour; esp_time.minute = minute; esp_time.second = 0; } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.year = year; esp_time.month = month; esp_time.day_of_month = day; diff --git a/esphome/loader.py b/esphome/loader.py index ec2f5101da..387443c032 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -82,11 +82,10 @@ class ComponentManifest: return getattr(self.module, "CONFLICTS_WITH", []) @property - def auto_load(self) -> list[str]: - al = getattr(self.module, "AUTO_LOAD", []) - if callable(al): - return al() - return al + def auto_load( + self, + ) -> list[str] | Callable[[], list[str]] | Callable[[ConfigType], list[str]]: + return getattr(self.module, "AUTO_LOAD", []) @property def codeowners(self) -> list[str]: diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c7da01075d..a5a6411c2b 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -75,6 +75,9 @@ FILTER_PLATFORMIO_LINES = [ r"Creating BIN file .*", r"Warning! Could not find file \".*.crt\"", r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.", + r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.", + r"Warning: esp-idf-size exited with code 2", + r"esp_idf_size: error: unrecognized arguments: --ng", ] diff --git a/platformio.ini b/platformio.ini index d97607fac5..44b466a2b3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -72,7 +72,6 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr @@ -107,6 +106,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) @@ -129,7 +129,7 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip -framework = arduino +framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first @@ -147,7 +147,7 @@ lib_deps = makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard - esphome/esp-audio-libs@1.1.4 ; audio + esphome/esp-audio-libs@2.0.1 ; audio build_flags = ${common:arduino.build_flags} @@ -170,7 +170,7 @@ lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.2 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word - esphome/esp-audio-libs@1.1.4 ; audio + esphome/esp-audio-libs@2.0.1 ; audio build_flags = ${common:idf.build_flags} -Wno-nonnull-compare @@ -193,6 +193,7 @@ platform_packages = framework = arduino lib_deps = ${common:arduino.lib_deps} + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -207,7 +208,8 @@ platform = libretiny@1.9.1 framework = arduino lib_compat_mode = soft lib_deps = - droscy/esp_wireguard@0.4.2 ; wireguard + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base + droscy/esp_wireguard@0.4.2 ; wireguard build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY @@ -274,6 +276,7 @@ build_unflags = [env:esp32-arduino-tidy] extends = common:esp32-arduino board = esp32dev +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-arduino-tidy build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} diff --git a/requirements.txt b/requirements.txt index 0b6820e7b5..7ff4a6eeb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,8 +12,8 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20250904.0 -aioesphomeapi==41.11.0 -zeroconf==0.147.2 +aioesphomeapi==41.13.0 +zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml.clib==0.2.12 # dashboard_import diff --git a/requirements_test.txt b/requirements_test.txt index 59ea77fd2d..76a305367a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ -pylint==3.3.8 +pylint==3.3.9 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.13.2 # also change in .pre-commit-config.yaml when updating +ruff==0.14.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/clang_tidy_hash.py b/script/clang_tidy_hash.py index 19eb2a825e..d0d8438437 100755 --- a/script/clang_tidy_hash.py +++ b/script/clang_tidy_hash.py @@ -48,9 +48,10 @@ def parse_requirement_line(line: str) -> tuple[str, str] | None: return None -def get_clang_tidy_version_from_requirements() -> str: +def get_clang_tidy_version_from_requirements(repo_root: Path | None = None) -> str: """Get clang-tidy version from requirements_dev.txt""" - requirements_path = Path(__file__).parent.parent / "requirements_dev.txt" + repo_root = _ensure_repo_root(repo_root) + requirements_path = repo_root / "requirements_dev.txt" lines = read_file_lines(requirements_path) for line in lines: @@ -68,30 +69,49 @@ def read_file_bytes(path: Path) -> bytes: return f.read() -def calculate_clang_tidy_hash() -> str: +def get_repo_root() -> Path: + """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""" + repo_root = _ensure_repo_root(repo_root) + hasher = hashlib.sha256() # Hash .clang-tidy file - clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy" + clang_tidy_path = repo_root / ".clang-tidy" content = read_file_bytes(clang_tidy_path) hasher.update(content) # Hash clang-tidy version from requirements_dev.txt - version = get_clang_tidy_version_from_requirements() + version = get_clang_tidy_version_from_requirements(repo_root) hasher.update(version.encode()) # Hash the entire platformio.ini file - platformio_path = Path(__file__).parent.parent / "platformio.ini" + platformio_path = repo_root / "platformio.ini" platformio_content = read_file_bytes(platformio_path) 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() -def read_stored_hash() -> str | None: +def read_stored_hash(repo_root: Path | None = None) -> str | None: """Read the stored hash from file""" - hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" + repo_root = _ensure_repo_root(repo_root) + hash_file = repo_root / ".clang-tidy.hash" if hash_file.exists(): lines = read_file_lines(hash_file) return lines[0].strip() if lines else None @@ -104,9 +124,10 @@ def write_file_content(path: Path, content: str) -> None: f.write(content) -def write_hash(hash_value: str) -> None: +def write_hash(hash_value: str, repo_root: Path | None = None) -> None: """Write hash to file""" - hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" + repo_root = _ensure_repo_root(repo_root) + hash_file = repo_root / ".clang-tidy.hash" # Strip any trailing newlines to ensure consistent formatting write_file_content(hash_file, hash_value.strip() + "\n") @@ -134,8 +155,28 @@ def main() -> None: stored_hash = read_stored_hash() if args.check: - # Exit 0 if full scan needed (hash changed or no hash file) - sys.exit(0 if current_hash != stored_hash else 1) + # Check if hash changed OR if .clang-tidy.hash was updated in this PR + # This is used in CI to determine if a full clang-tidy scan is needed + 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: write_hash(current_hash) @@ -151,15 +192,6 @@ def main() -> None: print("Clang-tidy hash unchanged") 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: print(f"Current hash: {current_hash}") print(f"Stored hash: {stored_hash}") diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 152a480d23..81b78b04be 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -7,9 +7,10 @@ import subprocess import sys import tempfile -from esphome.components.esp32 import ESP_IDF_PLATFORM_VERSION as ver +from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP from esphome.helpers import write_file_if_changed +ver = PLATFORM_VERSION_LOOKUP["recommended"] version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" root = Path(__file__).parent.parent boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py" diff --git a/script/helpers.py b/script/helpers.py index 38e6fcbd1e..61306b9489 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -529,7 +529,16 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: new_components.update(dep.split(".")[0] for dep in comp.dependencies) # Add auto_load components - new_components.update(comp.auto_load) + auto_load = 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 new_components -= all_components diff --git a/script/list-components.py b/script/list-components.py index ef02aecdf6..9ab1cdd852 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +from collections.abc import Callable from pathlib import Path import sys @@ -13,7 +14,7 @@ from esphome.const import ( PLATFORM_ESP8266, ) from esphome.core import CORE -from esphome.loader import get_component, get_platform +from esphome.loader import ComponentManifest, get_component, get_platform def filter_component_files(str): @@ -45,6 +46,29 @@ def add_item_to_components_graph(components_graph, parent, 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(): # The root directory of the repo root = Path(__file__).parent.parent @@ -63,7 +87,7 @@ def create_components_graph(): components_graph = {} platforms = [] - components = [] + components: list[tuple[ComponentManifest, str, Path]] = [] for path in components_dir.iterdir(): if not path.is_dir(): @@ -92,8 +116,8 @@ def create_components_graph(): for target_config in TARGET_CONFIGURATIONS: CORE.data[KEY_CORE] = target_config - for auto_load in comp.auto_load: - add_item_to_components_graph(components_graph, auto_load, name) + for item in resolve_auto_load(comp.auto_load, config=None): + add_item_to_components_graph(components_graph, item, name) # restore config CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] @@ -114,8 +138,8 @@ def create_components_graph(): for target_config in TARGET_CONFIGURATIONS: CORE.data[KEY_CORE] = target_config - for auto_load in platform.auto_load: - add_item_to_components_graph(components_graph, auto_load, name) + for item in resolve_auto_load(platform.auto_load, config={}): + add_item_to_components_graph(components_graph, item, name) # restore config CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] diff --git a/script/setup b/script/setup index 1bd7c44575..8cad7017ff 100755 --- a/script/setup +++ b/script/setup @@ -22,8 +22,6 @@ uv pip install -e ".[dev,test]" --config-settings editable_mode=compat pre-commit install -script/platformio_install_deps.py platformio.ini --libraries --tools --platforms - mkdir -p .temp echo diff --git a/script/setup.bat b/script/setup.bat index f89d5aea1a..003ea31b36 100644 --- a/script/setup.bat +++ b/script/setup.bat @@ -19,8 +19,6 @@ pip3 install -e ".[dev,test]" --config-settings editable_mode=compat pre-commit install -python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms - echo . echo . echo Virtual environment created. Run 'venv/Scripts/activate' to use it. diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 72ca3f6e9c..322efb701a 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -13,6 +13,7 @@ CONFIG_ESP_TASK_WDT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n +CONFIG_AUTOSTART_ARDUINO=y # esp32_ble CONFIG_BT_ENABLED=y diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 4f1693dac8..d87ae56ec2 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -10,6 +10,39 @@ esphome: data: message: Button was pressed - homeassistant.tag_scanned: pulse + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + type: hourly + capture_response: true + on_success: + - lambda: |- + JsonObjectConst next_hour = response["response"]["weather.forecast_home"]["forecast"][0]; + float next_temperature = next_hour["temperature"].as(); + 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(); + 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: port: 8000 diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..3d8d62a7ca --- /dev/null +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,15 @@ +spi: + clk_pin: GPIO7 + mosi_pin: GPIO9 + +display: + - platform: epaper_spi + model: 7.3in-spectra-e6 + cs_pin: GPIO5 + dc_pin: GPIO17 + reset_pin: GPIO16 + busy_pin: GPIO4 + rotation: 0 + update_interval: 60s + lambda: |- + it.circle(64, 64, 50, Color::BLACK); diff --git a/tests/components/esp32_can/test.esp32-c6-idf.yaml b/tests/components/esp32_can/test.esp32-c6-idf.yaml new file mode 100644 index 0000000000..6ef730c378 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-c6-idf.yaml @@ -0,0 +1,89 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + canbus_id: esp32_internal_can + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + canbus_id: esp32_internal_can + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Extended ID explicit + canbus_id: esp32_internal_can_2 + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + canbus_id: esp32_internal_can_2 + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: GPIO8 + tx_pin: GPIO7 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canbus1", "canid 500 %s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus1", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus1", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus1", "supply_fan_flow"); + break; + // to be continued... + } + - platform: esp32_can + id: esp32_internal_can_2 + rx_pin: GPIO10 + tx_pin: GPIO9 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canbus2", "canid 500 %s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus2", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus2", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus2", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/lm75b/common.yaml b/tests/components/lm75b/common.yaml new file mode 100644 index 0000000000..e451c2f679 --- /dev/null +++ b/tests/components/lm75b/common.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_lm75b + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: lm75b + name: LM75B Temperature + update_interval: 30s diff --git a/tests/components/lm75b/test.esp32-ard.yaml b/tests/components/lm75b/test.esp32-ard.yaml new file mode 100644 index 0000000000..43264df633 --- /dev/null +++ b/tests/components/lm75b/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO15 + sda_pin: GPIO13 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-c3-ard.yaml b/tests/components/lm75b/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-c3-idf.yaml b/tests/components/lm75b/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-idf.yaml b/tests/components/lm75b/test.esp32-idf.yaml new file mode 100644 index 0000000000..43264df633 --- /dev/null +++ b/tests/components/lm75b/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO15 + sda_pin: GPIO13 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp8266-ard.yaml b/tests/components/lm75b/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.rp2040-ard.yaml b/tests/components/lm75b/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/logger/common-default_uart.yaml b/tests/components/logger/common-default_uart.yaml index e8b56043eb..7939a5f9c5 100644 --- a/tests/components/logger/common-default_uart.yaml +++ b/tests/components/logger/common-default_uart.yaml @@ -6,11 +6,16 @@ esphome: format: "Warning: Logger level is %d" args: [id(logger_id).get_log_level()] - logger.set_level: WARN + - logger.set_level: + level: ERROR + tag: mqtt.client logger: id: logger_id level: DEBUG initial_level: INFO + logs: + mqtt.component: WARN select: - platform: logger diff --git a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml index 02767833a3..3129ca3143 100644 --- a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml +++ b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml @@ -25,6 +25,9 @@ mdns: - service: _http protocol: _tcp port: 80 + txt: + version: "1.0" + path: "/" # OTA should run at priority 54 (after mdns) ota: diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index 7d342ee353..c2b5ab737f 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -45,6 +45,22 @@ modbus_controller: printf("address=%d, value=%d", x); return true; max_cmd_retries: 0 + - id: modbus_controller4 + modbus_id: mod_bus2 + address: 0x4 + server_courtesy_response: + enabled: true + register_last_address: 100 + register_value: 0 + server_registers: + - address: 0x0001 + value_type: U_WORD + read_lambda: |- + return 0x8; + - address: 0x0005 + value_type: U_WORD + read_lambda: |- + return (random_uint32() % 100); binary_sensor: - platform: modbus_controller modbus_controller_id: modbus_controller1 diff --git a/tests/components/qmc5883l/common.yaml b/tests/components/qmc5883l/common.yaml index 5d8ac73b4f..c8ad4ba006 100644 --- a/tests/components/qmc5883l/common.yaml +++ b/tests/components/qmc5883l/common.yaml @@ -17,5 +17,7 @@ sensor: temperature: name: QMC5883L Temperature range: 800uT + data_rate: 200Hz oversampling: 256x update_interval: 15s + drdy_pin: ${drdy_pin} diff --git a/tests/components/qmc5883l/test.esp32-ard.yaml b/tests/components/qmc5883l/test.esp32-ard.yaml index 63c3bd6afd..2cf2041501 100644 --- a/tests/components/qmc5883l/test.esp32-ard.yaml +++ b/tests/components/qmc5883l/test.esp32-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO16 sda_pin: GPIO17 + drdy_pin: GPIO18 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-ard.yaml b/tests/components/qmc5883l/test.esp32-c3-ard.yaml index ee2c29ca4e..677501d15a 100644 --- a/tests/components/qmc5883l/test.esp32-c3-ard.yaml +++ b/tests/components/qmc5883l/test.esp32-c3-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO6 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-idf.yaml b/tests/components/qmc5883l/test.esp32-c3-idf.yaml index ee2c29ca4e..677501d15a 100644 --- a/tests/components/qmc5883l/test.esp32-c3-idf.yaml +++ b/tests/components/qmc5883l/test.esp32-c3-idf.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO6 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-idf.yaml b/tests/components/qmc5883l/test.esp32-idf.yaml index 63c3bd6afd..2cf2041501 100644 --- a/tests/components/qmc5883l/test.esp32-idf.yaml +++ b/tests/components/qmc5883l/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO16 sda_pin: GPIO17 + drdy_pin: GPIO18 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp8266-ard.yaml b/tests/components/qmc5883l/test.esp8266-ard.yaml index ee2c29ca4e..65b0fd75d9 100644 --- a/tests/components/qmc5883l/test.esp8266-ard.yaml +++ b/tests/components/qmc5883l/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.rp2040-ard.yaml b/tests/components/qmc5883l/test.rp2040-ard.yaml index ee2c29ca4e..65b0fd75d9 100644 --- a/tests/components/qmc5883l/test.rp2040-ard.yaml +++ b/tests/components/qmc5883l/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml index 10dd767598..cdeeab2c4a 100644 --- a/tests/components/remote_receiver/test.esp32-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-idf.yaml @@ -1,6 +1,8 @@ substitutions: pin: GPIO2 clock_resolution: "2000000" + carrier_duty_percent: "25" + carrier_frequency: "30000" filter_symbols: "2" receive_symbols: "4" rmt_symbols: "64" diff --git a/tests/integration/test_oversized_payloads.py b/tests/integration/test_oversized_payloads.py index 22167118af..ba18e3d348 100644 --- a/tests/integration/test_oversized_payloads.py +++ b/tests/integration/test_oversized_payloads.py @@ -15,7 +15,7 @@ async def test_oversized_payload_plaintext( run_compiled: RunCompiledFunction, api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory, ) -> None: - """Test that oversized payloads (>2304 bytes) from client cause disconnection without crashing.""" + """Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing.""" process_exited = False helper_log_found = False @@ -39,8 +39,8 @@ async def test_oversized_payload_plaintext( assert device_info is not None assert device_info.name == "oversized-plaintext" - # Create an oversized payload (>2304 bytes which is our new limit) - oversized_data = b"X" * 3000 # ~3KiB, exceeds the 2304 byte limit + # Create an oversized payload (>32768 bytes which is our new limit) + oversized_data = b"X" * 40000 # ~40KiB, exceeds the 32768 byte limit # Access the internal connection to send raw data frame_helper = client._connection._frame_helper @@ -161,8 +161,8 @@ async def test_oversized_payload_noise( assert device_info is not None assert device_info.name == "oversized-noise" - # Create an oversized payload (>2304 bytes which is our new limit) - oversized_data = b"Y" * 3000 # ~3KiB, exceeds the 2304 byte limit + # Create an oversized payload (>32768 bytes which is our new limit) + oversized_data = b"Y" * 40000 # ~40KiB, exceeds the 32768 byte limit # Access the internal connection to send raw data frame_helper = client._connection._frame_helper diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py index 2f84d11a0d..b1690a6a2d 100644 --- a/tests/script/test_clang_tidy_hash.py +++ b/tests/script/test_clang_tidy_hash.py @@ -44,37 +44,53 @@ def test_get_clang_tidy_version_from_requirements( assert result == expected -def test_calculate_clang_tidy_hash() -> None: - """Test calculating hash from all configuration sources.""" +def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None: + """Test calculating hash from all configuration sources including sdkconfig.defaults.""" clang_tidy_content = b"Checks: '-*,readability-*'\n" requirements_version = "clang-tidy==18.1.5" 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_hasher = hashlib.sha256() expected_hasher.update(clang_tidy_content) expected_hasher.update(requirements_version.encode()) expected_hasher.update(platformio_content) + expected_hasher.update(sdkconfig_content) expected_hash = expected_hasher.hexdigest() - # 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"" + result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path) - mock_read_bytes.side_effect = read_file_mock - result = clang_tidy_hash.calculate_clang_tidy_hash() + assert result == expected_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 @@ -85,67 +101,63 @@ def test_read_stored_hash_exists(tmp_path: Path) -> None: hash_file = tmp_path / ".clang-tidy.hash" hash_file.write_text(f"{stored_hash}\n") - 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() + result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path) assert result == stored_hash -def test_read_stored_hash_not_exists() -> None: +def test_read_stored_hash_not_exists(tmp_path: Path) -> None: """Test reading hash when file doesn't exist.""" - 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() + result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path) assert result is None -def test_write_hash() -> None: +def test_write_hash(tmp_path: Path) -> None: """Test writing hash to file.""" hash_value = "abc123def456" + hash_file = tmp_path / ".clang-tidy.hash" - with patch("clang_tidy_hash.write_file_content") as mock_write: - clang_tidy_hash.write_hash(hash_value) + clang_tidy_hash.write_hash(hash_value, repo_root=tmp_path) - # Verify write_file_content was called with correct parameters - 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" + assert hash_file.exists() + assert hash_file.read_text() == hash_value.strip() + "\n" @pytest.mark.parametrize( - ("args", "current_hash", "stored_hash", "expected_exit"), + ("args", "current_hash", "stored_hash", "hash_file_in_changed", "expected_exit"), [ - (["--check"], "abc123", "abc123", 1), # Hashes match, no scan needed - (["--check"], "abc123", "def456", 0), # Hashes differ, scan needed - (["--check"], "abc123", None, 0), # No stored hash, scan needed + (["--check"], "abc123", "abc123", False, 1), # Hashes match, no scan needed + (["--check"], "abc123", "def456", False, 0), # Hashes differ, scan needed + (["--check"], "abc123", None, False, 0), # No stored hash, scan needed + ( + ["--check"], + "abc123", + "abc123", + True, + 0, + ), # Hash file updated in PR, scan needed ], ) def test_main_check_mode( - args: list[str], current_hash: str, stored_hash: str | None, expected_exit: int + args: list[str], + current_hash: str, + stored_hash: str | None, + hash_file_in_changed: bool, + expected_exit: int, ) -> None: """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 ( patch("sys.argv", ["clang_tidy_hash.py"] + args), patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_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, ): clang_tidy_hash.main() diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index e8d9c02524..932221997c 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -101,3 +101,10 @@ def mock_get_idedata() -> Generator[Mock, None, None]: """Mock get_idedata for platformio_api.""" with patch("esphome.platformio_api.get_idedata") as 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 diff --git a/tests/unit_tests/fixtures/auto_load_dynamic.yaml b/tests/unit_tests/fixtures/auto_load_dynamic.yaml new file mode 100644 index 0000000000..b604a2a42b --- /dev/null +++ b/tests/unit_tests/fixtures/auto_load_dynamic.yaml @@ -0,0 +1,10 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + +# Test component with dynamic AUTO_LOAD +test_component: + enable_logger: true + enable_api: false diff --git a/tests/unit_tests/fixtures/auto_load_static.yaml b/tests/unit_tests/fixtures/auto_load_static.yaml new file mode 100644 index 0000000000..c8f9e6222a --- /dev/null +++ b/tests/unit_tests/fixtures/auto_load_static.yaml @@ -0,0 +1,8 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + +# Test component with static AUTO_LOAD +test_component: diff --git a/tests/unit_tests/test_config_auto_load.py b/tests/unit_tests/test_config_auto_load.py new file mode 100644 index 0000000000..d31b17eeec --- /dev/null +++ b/tests/unit_tests/test_config_auto_load.py @@ -0,0 +1,131 @@ +"""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 diff --git a/tests/unit_tests/test_config_normalization.py b/tests/unit_tests/test_config_normalization.py index 4b79ddd426..d70f3c24e0 100644 --- a/tests/unit_tests/test_config_normalization.py +++ b/tests/unit_tests/test_config_normalization.py @@ -10,13 +10,6 @@ from esphome import config, yaml_util 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 def mock_get_platform() -> Generator[Mock, None, None]: """Fixture for mocking get_platform."""