1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-12 14:53:49 +01:00

Merge branch 'integration' of https://github.com/esphome/esphome into integration

This commit is contained in:
J. Nick Koston
2025-10-09 10:32:52 -10:00
220 changed files with 4098 additions and 1801 deletions

View File

@@ -1 +1 @@
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f

View File

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

View File

@@ -466,7 +466,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} 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: env:
SKIP: pylint,clang-tidy-hash SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Stale - name: Stale
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with: with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true remove-stale-when-updated: true

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.13.2 rev: v0.14.0
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77 esphome/components/ens210/* @itn3rd77
esphome/components/epaper_spi/* @esphome/core
esphome/components/es7210/* @kahrendt esphome/components/es7210/* @kahrendt
esphome/components/es7243e/* @kbx81 esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81 esphome/components/es8156/* @kbx81
@@ -256,6 +257,7 @@ esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core esphome/components/light/* @esphome/core
esphome/components/lightwaverf/* @max246 esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lm75b/* @beormund
esphome/components/ln882x/* @lamauny esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @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/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow
esphome/components/split_buffer/* @jesserockz
esphome/components/sprinkler/* @kbx81 esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_base/* @kbx81

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # 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 # 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 # for a project that appears at the top of each page and should give viewer a

View File

@@ -14,9 +14,11 @@ from typing import Protocol
import argcomplete 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 from esphome import const, writer, yaml_util
import esphome.codegen as cg 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.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import ( from esphome.const import (
ALLOWED_NAME_CHARS, ALLOWED_NAME_CHARS,
@@ -240,6 +242,8 @@ def has_ota() -> bool:
def has_mqtt_ip_lookup() -> bool: def has_mqtt_ip_lookup() -> bool:
"""Check if MQTT is available and IP lookup is supported.""" """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: if CONF_MQTT not in CORE.config:
return False return False
# Default Enabled # Default Enabled

View File

@@ -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_; } int Animation::get_current_frame() const { return this->current_frame_; }
void Animation::next_frame() { void Animation::next_frame() {
this->current_frame_++; this->current_frame_++;
if (loop_count_ && this->current_frame_ == loop_end_frame_ && if (loop_count_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ &&
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
this->current_frame_ = loop_start_frame_; this->current_frame_ = loop_start_frame_;
this->loop_current_iteration_++; this->loop_current_iteration_++;
} }
if (this->current_frame_ >= animation_frame_count_) { if (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) {
this->loop_current_iteration_ = 1; this->loop_current_iteration_ = 1;
this->current_frame_ = 0; this->current_frame_ = 0;
} }

View File

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

View File

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

View File

@@ -8,9 +8,9 @@
#endif #endif
#include <cerrno> #include <cerrno>
#include <cinttypes> #include <cinttypes>
#include <utility>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <utility>
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
@@ -116,8 +116,7 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
this->log_warning_(LOG_STR("Helper init failed"), err);
return; return;
} }
this->client_info_.peername = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
@@ -147,8 +146,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
this->log_socket_operation_failed_(err);
return; return;
} }
@@ -163,17 +161,13 @@ void APIConnection::loop() {
// No more data available // No more data available
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
this->log_warning_(LOG_STR("Reading failed"), err);
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
// read a packet // read a packet
if (buffer.data_len > 0) { this->read_message(buffer.data_len, buffer.type,
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->flags_.remove) if (this->flags_.remove)
return; return;
} }
@@ -1395,6 +1389,11 @@ void APIConnection::complete_authentication_() {
this->send_time_request(); this->send_time_request();
} }
#endif #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) { bool APIConnection::send_hello_response(const HelloRequest &msg) {
@@ -1550,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
} }
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (msg.response_data_len > 0) {
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
msg.response_data_len);
} else
#endif
{
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
}
};
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
NoiseEncryptionSetKeyResponse resp; NoiseEncryptionSetKeyResponse resp;
@@ -1580,8 +1593,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
delay(0); delay(0);
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
this->log_socket_operation_failed_(err);
return false; return false;
} }
if (this->helper_->can_write_without_blocking()) 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) if (err == APIError::WOULD_BLOCK)
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
this->log_warning_(LOG_STR("Packet write failed"), err);
return false; return false;
} }
// Do not set last_traffic_ on send // 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}, APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
std::span<const PacketInfo>(packet_info, packet_count)); std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
this->log_warning_(LOG_STR("Batch write failed"), err);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #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); 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 } // namespace esphome::api
#endif #endif

View File

@@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection {
return; return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); 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 #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
@@ -732,8 +735,11 @@ class APIConnection final : public APIServerConnection {
// Helper function to log API errors with errno // Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err); void log_warning_(const LogString *message, APIError err);
// Specific helper for duplicated error message // Helper to handle fatal errors with logging
void log_socket_operation_failed_(APIError err); inline void fatal_error_with_log_(const LogString *message, APIError err) {
this->on_fatal_error();
this->log_warning_(message, err);
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -19,13 +19,14 @@ namespace esphome::api {
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices // Maximum message size limits to prevent OOM on constrained devices
// Voice Assistant is our largest user at 1024 bytes per audio chunk // Handshake messages are limited to a small size for security
// Using 2048 + 256 bytes overhead = 2304 bytes total to support voice and future needs static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
// ESP8266 has very limited RAM and cannot support voice assistant
// Data message limits vary by platform based on available memory
#ifdef USE_ESP8266 #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 #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 #endif
// Forward declaration // Forward declaration

View File

@@ -132,26 +132,16 @@ APIError APINoiseFrameHelper::loop() {
return APIFrameHelper::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. * @return APIError::OK if a full packet is in rx_buf_
* 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.
* *
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet. * errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/ */
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) { APIError APINoiseFrameHelper::try_read_frame_() {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < 3) {
// no header information yet // no header information yet
@@ -178,23 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
// read body // read body
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) { // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
// for handshake message only permit up to 128 bytes uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
if (msg_size > limit) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size); HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
return APIError::BAD_HANDSHAKE_PACKET_LEN; return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
} }
// Check against maximum message size to prevent OOM // Reserve space for body
if (msg_size > MAX_MESSAGE_SIZE) { if (this->rx_buf_.size() != msg_size) {
state_ = State::FAILED; this->rx_buf_.resize(msg_size);
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);
} }
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
@@ -212,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
} }
} }
LOG_PACKET_RECEIVED(rx_buf_); LOG_PACKET_RECEIVED(this->rx_buf_);
*frame = std::move(rx_buf_);
// consume msg // Clear state for next frame (rx_buf_ still contains data for caller)
rx_buf_ = {}; this->rx_buf_len_ = 0;
rx_buf_len_ = 0; this->rx_header_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK; return APIError::OK;
} }
@@ -239,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::CLIENT_HELLO) { if (state_ == State::CLIENT_HELLO) {
// waiting for client hello // waiting for client hello
std::vector<uint8_t> frame; aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
// ignore contents, may be used in future for flags // ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data // Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = prologue_.size(); size_t old_size = this->prologue_.size();
prologue_.resize(old_size + 2 + frame.size()); this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
prologue_[old_size] = (uint8_t) (frame.size() >> 8); this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
prologue_[old_size + 1] = (uint8_t) frame.size(); this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size()); std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
state_ = State::SERVER_HELLO; state_ = State::SERVER_HELLO;
} }
@@ -292,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() {
int action = noise_handshakestate_get_action(handshake_); int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) { if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg // waiting for handshake msg
std::vector<uint8_t> frame; aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
if (frame.empty()) { if (this->rx_buf_.empty()) {
send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame[0] != 0x00) { } else if (this->rx_buf_[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]); HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} }
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, 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); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) { if (err != 0) {
// Special handling for MAC failure // Special handling for MAC failure
@@ -386,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err; APIError aerr = this->state_action_();
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return aerr; return aerr;
} }
if (state_ != State::DATA) { if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
std::vector<uint8_t> frame; aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
APIError decrypt_err = APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK) if (decrypt_err != APIError::OK) {
return decrypt_err; return decrypt_err;
}
uint16_t msg_size = mbuf.size; uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.data(); uint8_t *msg_data = this->rx_buf_.data();
if (msg_size < 4) { if (msg_size < 4) {
state_ = State::FAILED; this->state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size); HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
@@ -422,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) { if (data_len > msg_size - 4) {
state_ = State::FAILED; this->state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
buffer->container = std::move(frame); buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 4; buffer->data_offset = 4;
buffer->data_len = data_len; buffer->data_len = data_len;
buffer->type = type; buffer->type = type;

View File

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

View File

@@ -47,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() {
return APIFrameHelper::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: store the parsed frame in that struct
* *
* @return See APIError * @return See APIError
* *
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/ */
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) { APIError APIPlaintextFrameHelper::try_read_frame_() {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header // read header
while (!rx_header_parsed_) { while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes // 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<uint8_t> *frame) {
} }
// header reading done // header reading done
// reserve space for body // Reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) { if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_); this->rx_buf_.resize(this->rx_header_parsed_len_);
} }
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
@@ -170,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
} }
} }
LOG_PACKET_RECEIVED(rx_buf_); LOG_PACKET_RECEIVED(this->rx_buf_);
*frame = std::move(rx_buf_);
// consume msg // Clear state for next frame (rx_buf_ still contains data for caller)
rx_buf_ = {}; this->rx_buf_len_ = 0;
rx_buf_len_ = 0; this->rx_header_buf_pos_ = 0;
rx_header_buf_pos_ = 0; this->rx_header_parsed_ = false;
rx_header_parsed_ = false;
return APIError::OK; 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; return APIError::WOULD_BLOCK;
} }
std::vector<uint8_t> frame; APIError aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) { if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't // Make sure to tell the remote that we don't
@@ -220,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr; return aerr;
} }
buffer->container = std::move(frame); buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 0; buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_; buffer->data_len = this->rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_; buffer->type = this->rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,16 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#include "esphome/core/version.h" #include "esphome/core/version.h"
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "homeassistant_service.h"
#endif
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
#endif #endif
#include <algorithm> #include <algorithm>
#include <utility>
namespace esphome::api { namespace esphome::api {
@@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
client->send_homeassistant_action(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 #ifdef USE_API_HOMEASSISTANT_STATES
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,

View File

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

View File

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

View File

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

View File

@@ -165,4 +165,4 @@ def final_validate_audio_schema(
async def to_code(config): 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")

View File

@@ -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, void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale) { size_t samples_to_scale) {
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. // 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; int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
output_buffer[i] = (int16_t) (acc >> 15); output_buffer[i] = (int16_t) (acc >> 15);
} }

View File

@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available()); this->input_transfer_buffer_->available());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::POTENTIALLY_FAILED; // Serrious error reading FLAC header, there is no recovery
}
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
// Couldn't read FLAC header
return FileDecoderState::FAILED; return FileDecoderState::FAILED;
} }
size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); 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 // Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
} }
uint32_t output_samples = 0; uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame( auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), this->input_transfer_buffer_->available(),
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples); this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { 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. // Not an issue, just needs more data that we'll get next time.

View File

@@ -97,10 +97,10 @@ void BL0906::handle_actions_() {
return; return;
} }
ActionCallbackFuncPtr ptr_func = nullptr; 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]; ptr_func = this->action_queue_[i];
if (ptr_func) { if (ptr_func) {
ESP_LOGI(TAG, "HandleActionCallback[%d]", i); ESP_LOGI(TAG, "HandleActionCallback[%zu]", i);
(this->*ptr_func)(); (this->*ptr_func)();
} }
} }

View File

@@ -51,7 +51,7 @@ void BL0942::loop() {
if (!avail) { if (!avail) {
return; return;
} }
if (avail < sizeof(buffer)) { if (static_cast<size_t>(avail) < sizeof(buffer)) {
if (!this->rx_start_) { if (!this->rx_start_) {
this->rx_start_ = millis(); this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
@@ -148,7 +148,7 @@ void BL0942::setup() {
this->write_reg_(BL0942_REG_USR_WRPROT, 0); this->write_reg_(BL0942_REG_USR_WRPROT, 0);
if (this->read_reg_(BL0942_REG_MODE) != mode) if (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode)
this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->status_set_warning(LOG_STR("BL0942 setup failed!"));
this->flush(); this->flush();

View File

@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_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" CONF_BLE_CLIENT_ID = "ble_client_id"

View File

@@ -42,9 +42,7 @@ def validate_connections(config):
) )
elif config[CONF_ACTIVE]: elif config[CONF_ACTIVE]:
connection_slots: int = config[CONF_CONNECTION_SLOTS] connection_slots: int = config[CONF_CONNECTION_SLOTS]
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
config
)
return { return {
**config, **config,
@@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All(
default=DEFAULT_CONNECTION_SLOTS, default=DEFAULT_CONNECTION_SLOTS,
): cv.All( ): cv.All(
cv.positive_int, 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.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA), 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),
), ),
} }
) )

View File

@@ -11,14 +11,14 @@ namespace captive_portal {
static const char *const TAG = "captive_portal"; static const char *const TAG = "captive_portal";
void CaptivePortal::handle_config(AsyncWebServerRequest *request) { void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
#ifdef USE_ESP8266 #ifdef USE_ESP8266
stream->print(F("{\"mac\":\"")); stream->print(ESPHOME_F("{\"mac\":\""));
stream->print(get_mac_address_pretty().c_str()); 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(App.get_name().c_str());
stream->print(F("\",\"aps\":[{}")); stream->print(ESPHOME_F("\",\"aps\":[{}"));
#else #else
stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
#endif #endif
@@ -29,19 +29,19 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
// Assumes no " in ssid, possible unicode isses? // Assumes no " in ssid, possible unicode isses?
#ifdef USE_ESP8266 #ifdef USE_ESP8266
stream->print(F(",{\"ssid\":\"")); stream->print(ESPHOME_F(",{\"ssid\":\""));
stream->print(scan.get_ssid().c_str()); stream->print(scan.get_ssid().c_str());
stream->print(F("\",\"rssi\":")); stream->print(ESPHOME_F("\",\"rssi\":"));
stream->print(scan.get_rssi()); stream->print(scan.get_rssi());
stream->print(F(",\"lock\":")); stream->print(ESPHOME_F(",\"lock\":"));
stream->print(scan.get_with_auth()); stream->print(scan.get_with_auth());
stream->print(F("}")); stream->print(ESPHOME_F("}"));
#else #else
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
scan.get_with_auth()); scan.get_with_auth());
#endif #endif
} }
stream->print(F("]}")); stream->print(ESPHOME_F("]}"));
request->send(stream); request->send(stream);
} }
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { 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()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->save_wifi_sta(ssid, psk);
wifi::global_wifi_component->start_scanning(); wifi::global_wifi_component->start_scanning();
request->redirect(F("/?save")); request->redirect(ESPHOME_F("/?save"));
} }
void CaptivePortal::setup() { void CaptivePortal::setup() {
@@ -75,7 +75,7 @@ void CaptivePortal::start() {
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
this->dns_server_ = make_unique<DNSServer>(); this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
this->dns_server_->start(53, F("*"), ip); this->dns_server_->start(53, ESPHOME_F("*"), ip);
#endif #endif
this->initialized_ = true; this->initialized_ = true;
@@ -88,10 +88,10 @@ void CaptivePortal::start() {
} }
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
if (req->url() == F("/config.json")) { if (req->url() == ESPHOME_F("/config.json")) {
this->handle_config(req); this->handle_config(req);
return; return;
} else if (req->url() == F("/wifisave")) { } else if (req->url() == ESPHOME_F("/wifisave")) {
this->handle_wifisave(req); this->handle_wifisave(req);
return; return;
} }
@@ -100,11 +100,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
// This includes OS captive portal detection endpoints which will trigger // This includes OS captive portal detection endpoints which will trigger
// the captive portal when they don't receive their expected responses // the captive portal when they don't receive their expected responses
#ifndef USE_ESP8266 #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 #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 #endif
response->addHeader(F("Content-Encoding"), F("gzip")); response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
req->send(response); req->send(response);
} }

View File

@@ -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 cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0; uint8_t crc = 0;
for (int i = 0; i < len - 1; i++) { for (size_t i = 0; i < len - 1; i++) {
crc -= response[i]; crc -= response[i];
} }
return crc; return crc;

View File

@@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() {
uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00}; uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
// Calculate checksum // 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]; remote_header[sizeof(remote_header) - 1] += remote_header[i];
} }
@@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() {
remote_state[9] = fan_speed & 0xff; remote_state[9] = fan_speed & 0xff;
// Calculate checksum // 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]; 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; bool valid_daikin_frame = false;
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true; valid_daikin_frame = true;
int bytes_count = data.size() / 2 / 8; size_t bytes_count = data.size() / 2 / 8;
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]); std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
buf[0] = '\0'; buf[0] = '\0';
for (size_t i = 0; i < bytes_count; i++) { 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) { if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1]; char sbuf[16 * 10 + 1];
sbuf[0] = '\0'; sbuf[0] = '\0';
for (size_t j = 0; j < data.size(); j++) { for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
if ((j - 2) % 16 == 0) { if ((j - 2) % 16 == 0) {
if (j > 0) { if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); 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 = ' '; char type_ch = ' ';
// debug_tolerance = 25% // 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<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] &&
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
type_ch = 'P'; 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<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
type_ch = 'a'; type_ch = 'a';
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] &&
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
type_ch = 'H'; type_ch = 'H';
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
type_ch = 'h'; type_ch = 'h';
if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] &&
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
type_ch = 'B'; type_ch = 'B';
if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
type_ch = '1'; type_ch = '1';
if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)))
type_ch = '0'; type_ch = '0';
if (abs(data[j]) > 100000) { if (abs(data[j]) > 100000) {
@@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
} else { } else {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
} }
if (j == data.size() - 1) { if (j + 1 == static_cast<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
} }
} }

View File

@@ -5,7 +5,7 @@ namespace dashboard_import {
static std::string g_package_import_url; // NOLINT 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); } void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
} // namespace dashboard_import } // namespace dashboard_import

View File

@@ -5,7 +5,7 @@
namespace esphome { namespace esphome {
namespace dashboard_import { namespace dashboard_import {
std::string get_package_import_url(); const std::string &get_package_import_url();
void set_package_import_url(std::string url); void set_package_import_url(std::string url);
} // namespace dashboard_import } // namespace dashboard_import

View File

@@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]

View File

@@ -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]))

View File

@@ -0,0 +1,227 @@
#include "epaper_spi.h"
#include <cinttypes>
#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

View File

@@ -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 <queue>
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<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
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<EPaperState> state_queue_{{EPaperState::IDLE}};
};
} // namespace esphome::epaper_spi

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) {
} }
bool ES7210::configure_sample_rate_() { 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; 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) if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
coeff = i; coeff = static_cast<int>(i);
} }
if (coeff >= 0) { if (coeff >= 0) {

View File

@@ -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" return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
def _format_framework_espidf_version( def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
ver: cv.Version, release: str, for_platformio: bool # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
) -> str:
# format the given arduino (https://github.com/espressif/esp-idf/releases) version to
# a PIO platformio/framework-espidf value # 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: 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)}.{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" 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 # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
# The platform-espressif32 version to use for arduino frameworks "recommended": cv.Version(3, 2, 1),
# - https://github.com/pioarduino/platform-espressif32/releases "latest": cv.Version(3, 3, 1),
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") "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 # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) "recommended": cv.Version(5, 4, 2),
# The platformio/espressif32 version to use for esp-idf frameworks "latest": cv.Version(5, 5, 1),
# - https://github.com/platformio/platform-espressif32/releases "dev": cv.Version(5, 5, 1),
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 }
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") 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 # The platform-espressif32 version
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ # - https://github.com/pioarduino/platform-espressif32/releases
cv.Version(5, 3, 1), PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 3, 0), "recommended": cv.Version(54, 3, 21, "2"),
cv.Version(5, 2, 2), "latest": cv.Version(55, 3, 31),
cv.Version(5, 2, 1), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
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),
]
def _check_versions(value): def _check_versions(value):
value = value.copy() 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 value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
if CONF_SOURCE in value: 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, 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:
raise cv.Invalid( 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: else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) 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_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"): if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
# prefix is necessary or platformio will complain with a cryptic error if version < cv.Version(3, 0, 0):
value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}" 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( _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." "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): def _parse_platform_version(value):
try: try:
ver = cv.Version.parse(cv.version_number(value)) 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}"
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" if ver.extra:
if ver.extra: release += f"-{ver.extra}"
release += f"-{ver.extra}" return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
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}"
except cv.Invalid: except cv.Invalid:
return value 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): def _detect_variant(value):
board = value.get(CONF_BOARD) board = value.get(CONF_BOARD)
variant = value.get(CONF_VARIANT) variant = value.get(CONF_VARIANT)
@@ -808,6 +748,8 @@ async def to_code(config):
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) 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]: if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_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_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_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True

View File

@@ -1,5 +1,8 @@
from collections.abc import Callable, MutableMapping
from enum import Enum from enum import Enum
import logging
import re import re
from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
@@ -9,16 +12,19 @@ from esphome.const import (
CONF_ENABLE_ON_BOOT, CONF_ENABLE_ON_BOOT,
CONF_ESPHOME, CONF_ESPHOME,
CONF_ID, CONF_ID,
CONF_MAX_CONNECTIONS,
CONF_NAME, CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX, CONF_NAME_ADD_MAC_SUFFIX,
) )
from esphome.core import TimePeriod from esphome.core import CORE, TimePeriod
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"
_LOGGER = logging.getLogger(__name__)
class BTLoggers(Enum): class BTLoggers(Enum):
"""Bluetooth logger categories available in ESP-IDF. """Bluetooth logger categories available in ESP-IDF.
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
CONF_CONNECTION_TIMEOUT = "connection_timeout" CONF_CONNECTION_TIMEOUT = "connection_timeout"
CONF_MAX_NOTIFICATIONS = "max_notifications" 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] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
@@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.positive_int, cv.positive_int,
cv.Range(min=1, max=64), 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) ).extend(cv.COMPONENT_SCHEMA)
@@ -230,6 +261,56 @@ def validate_variant(_):
raise cv.Invalid(f"{variant} does not support Bluetooth") 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): def final_validation(config):
validate_variant(config) validate_variant(config)
if (name := config.get(CONF_NAME)) is not None: 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 # Set GATT Client/Server sdkconfig options based on which components are loaded
full_config = fv.full_config.get() 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 # Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config has_ble_server = "esp32_ble_server" in full_config
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server) 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) 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 return config
@@ -270,6 +375,10 @@ async def to_code(config):
cg.add(var.set_name(name)) cg.add(var.set_name(name))
await cg.register_component(var, config) 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_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)

View File

@@ -68,6 +68,10 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
} }
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) { void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->set_manufacturer_data(data); this->advertising_->set_manufacturer_data(data);
this->advertising_start(); this->advertising_start();
@@ -213,15 +217,17 @@ bool ESP32BLE::ble_setup_() {
if (this->name_.has_value()) { if (this->name_.has_value()) {
name = this->name_.value(); name = this->name_.value();
if (App.is_name_add_mac_suffix_enabled()) { if (App.is_name_add_mac_suffix_enabled()) {
name += "-" + get_mac_address().substr(6); name += "-";
name += get_mac_address().substr(6);
} }
} else { } else {
name = App.get_name(); name = App.get_name();
if (name.length() > 20) { if (name.length() > 20) {
if (App.is_name_add_mac_suffix_enabled()) { 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 { } else {
name = name.substr(0, 20); name.resize(20);
} }
} }
} }

View File

@@ -118,6 +118,7 @@ class ESP32BLE : public Component {
void advertising_start(); void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data); void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name); void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,6 +59,10 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
} }
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) { void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data; delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr; this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size(); this->advertising_data_.manufacturer_len = data.size();
@@ -152,7 +156,7 @@ void BLEAdvertising::loop() {
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop(); this->stop();
this->current_adv_index_ += 1; this->current_adv_index_ += 1;
if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { if (static_cast<size_t>(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) {
this->current_adv_index_ = -1; this->current_adv_index_ = -1;
} }
this->start(); this->start();

View File

@@ -35,6 +35,7 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } 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_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data); void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data); void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data); void set_service_data(std::span<const uint8_t> data);

View File

@@ -42,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
ESPBTUUID ret; ESPBTUUID ret;
if (data.length() == 4) { if (data.length() == 4) {
ret.uuid_.len = ESP_UUID_LEN_16; // 16-bit UUID as 4-character hex string
ret.uuid_.uuid.uuid16 = 0; auto parsed = parse_hex<uint16_t>(data);
for (uint i = 0; i < data.length(); i += 2) { if (parsed.has_value()) {
uint8_t msb = data.c_str()[i]; ret.uuid_.len = ESP_UUID_LEN_16;
uint8_t lsb = data.c_str()[i + 1]; ret.uuid_.uuid.uuid16 = parsed.value();
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;
} }
} else if (data.length() == 8) { } else if (data.length() == 8) {
ret.uuid_.len = ESP_UUID_LEN_32; // 32-bit UUID as 8-character hex string
ret.uuid_.uuid.uuid32 = 0; auto parsed = parse_hex<uint32_t>(data);
for (uint i = 0; i < data.length(); i += 2) { if (parsed.has_value()) {
uint8_t msb = data.c_str()[i]; ret.uuid_.len = ESP_UUID_LEN_32;
uint8_t lsb = data.c_str()[i + 1]; ret.uuid_.uuid.uuid32 = parsed.value();
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;
} }
} else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
// investigated (lack of time) // investigated (lack of time)
@@ -145,28 +131,16 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
if (this->uuid_.len == uuid.uuid_.len) { if (this->uuid_.len == uuid.uuid_.len) {
switch (this->uuid_.len) { switch (this->uuid_.len) {
case ESP_UUID_LEN_16: case ESP_UUID_LEN_16:
if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16;
return true;
}
break;
case ESP_UUID_LEN_32: case ESP_UUID_LEN_32:
if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32;
return true;
}
break;
case ESP_UUID_LEN_128: case ESP_UUID_LEN_128:
for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) { return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0;
if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { default:
return false; return false;
}
}
return true;
break;
} }
} 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_; } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const { std::string ESPBTUUID::to_string() const {

View File

@@ -49,7 +49,11 @@ void BLECharacteristic::notify() {
this->service_->get_server()->get_connected_client_count() == 0) this->service_->get_server()->get_connected_client_count() == 0)
return; 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(); size_t length = this->value_.size();
// Find the client in the list of clients to notify // Find the client in the list of clients to notify
auto *entry = this->find_client_in_notify_list_(client); auto *entry = this->find_client_in_notify_list_(client);
@@ -121,69 +125,49 @@ bool BLECharacteristic::is_created() {
if (this->state_ != CREATING_DEPENDENTS) if (this->state_ != CREATING_DEPENDENTS)
return false; return false;
bool created = true;
for (auto *descriptor : this->descriptors_) { for (auto *descriptor : this->descriptors_) {
created &= descriptor->is_created(); if (!descriptor->is_created())
return false;
} }
if (created) // All descriptors are created if we reach here
this->state_ = CREATED; this->state_ = CREATED;
return this->state_ == CREATED; return true;
} }
bool BLECharacteristic::is_failed() { bool BLECharacteristic::is_failed() {
if (this->state_ == FAILED) if (this->state_ == FAILED)
return true; return true;
bool failed = false;
for (auto *descriptor : this->descriptors_) { 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) { void BLECharacteristic::set_broadcast_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, 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);
}
} }
void BLECharacteristic::set_indicate_property(bool value) { void BLECharacteristic::set_indicate_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, 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);
}
} }
void BLECharacteristic::set_notify_property(bool value) { void BLECharacteristic::set_notify_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, 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);
}
} }
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) { void BLECharacteristic::set_write_no_response_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, 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);
}
} }
void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,

View File

@@ -97,6 +97,8 @@ class BLECharacteristic {
void remove_client_from_notify_list_(uint16_t conn_id); void remove_client_from_notify_list_(uint16_t conn_id);
ClientNotificationEntry *find_client_in_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<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_; std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_; std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;

View File

@@ -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() { void BLEServer::ble_before_disabled_event_handler() {
// Delete all clients // Delete all clients
this->clients_.clear(); this->client_count_ = 0;
// Delete all services // Delete all services
for (auto &entry : this->services_) { for (auto &entry : this->services_) {
entry.service->do_delete(); entry.service->do_delete();

View File

@@ -12,7 +12,6 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <functional> #include <functional>
#ifdef USE_ESP32 #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; } void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
uint32_t get_connected_client_count() { return this->clients_.size(); } uint32_t get_connected_client_count() { return this->client_count_; }
const std::unordered_set<uint16_t> &get_clients() { return this->clients_; } 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, void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) override; esp_ble_gatts_cb_param_t *param) override;
@@ -82,8 +82,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void restart_advertising_(); void restart_advertising_();
void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } int8_t find_client_index_(uint16_t conn_id) const;
void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } void add_client_(uint16_t conn_id);
void remove_client_(uint16_t conn_id);
void dispatch_callbacks_(CallbackType type, uint16_t conn_id); void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
std::vector<CallbackEntry> callbacks_; std::vector<CallbackEntry> callbacks_;
@@ -92,7 +93,8 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};
bool registered_{false}; bool registered_{false};
std::unordered_set<uint16_t> clients_; uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0};
std::vector<ServiceEntry> services_{}; std::vector<ServiceEntry> services_{};
std::vector<BLEService *> services_to_start_{}; std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_{}; BLEService *device_information_service_{};

View File

@@ -1,14 +1,13 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, MutableMapping
import logging import logging
from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import ( from esphome.components.esp32_ble import (
IDF_MAX_CONNECTIONS,
BTLoggers, BTLoggers,
bt_uuid, bt_uuid,
bt_uuid16_format, bt_uuid16_format,
@@ -24,6 +23,7 @@ from esphome.const import (
CONF_INTERVAL, CONF_INTERVAL,
CONF_MAC_ADDRESS, CONF_MAC_ADDRESS,
CONF_MANUFACTURER_ID, CONF_MANUFACTURER_ID,
CONF_MAX_CONNECTIONS,
CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_ADVERTISE,
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
@@ -38,19 +38,12 @@ AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"] 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_ESP32_BLE_ID = "esp32_ble_id"
CONF_SCAN_PARAMETERS = "scan_parameters" CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_WINDOW = "window" CONF_WINDOW = "window"
CONF_ON_SCAN_END = "on_scan_end" CONF_ON_SCAN_END = "on_scan_end"
CONF_SOFTWARE_COEXISTENCE = "software_coexistence" CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
DEFAULT_MAX_CONNECTIONS = 3
IDF_MAX_CONNECTIONS = 9
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -128,6 +121,15 @@ def validate_scan_parameters(config):
return 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): def as_hex(value):
return cg.RawExpression(f"0x{value}ULL") 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( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), 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.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
), ),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( 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, cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
validate_max_connections_deprecated,
) )
def validate_remaining_connections(config): FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
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
)
ESP_BLE_DEVICE_SCHEMA = cv.Schema( ESP_BLE_DEVICE_SCHEMA = cv.Schema(
{ {
@@ -345,10 +298,8 @@ async def to_code(config):
# Match arduino CONFIG_BTU_TASK_STACK_SIZE # Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 # 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_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
add_idf_sdkconfig_option( # configured in esp32_ble component based on max_connections setting
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
)
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT") cg.add_define("USE_ESP32_BLE_CLIENT")

View File

@@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
} }
bool ESP32Can::setup_internal() { bool ESP32Can::setup_internal() {
static int next_twai_ctrl_num = 0;
if (static_cast<unsigned>(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_t g_config =
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); 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()) { if (this->tx_queue_len_.has_value()) {
g_config.tx_queue_len = this->tx_queue_len_.value(); g_config.tx_queue_len = this->tx_queue_len_.value();
} }
@@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() {
} }
// Install TWAI driver // 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 // Failed to install driver
this->mark_failed(); this->mark_failed();
return false; return false;
} }
// Start TWAI driver // Start TWAI driver
if (twai_start() != ESP_OK) { if (twai_start_v2(this->twai_handle_) != ESP_OK) {
// Failed to start driver // Failed to start driver
this->mark_failed(); this->mark_failed();
return false; return false;
@@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() {
} }
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { 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) { if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) {
return canbus::ERROR_FAILTX; 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); 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; return canbus::ERROR_OK;
} else { } else {
return canbus::ERROR_ALLTXBUSY; 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) { 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; 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; return canbus::ERROR_NOMSG;
} }

View File

@@ -5,6 +5,8 @@
#include "esphome/components/canbus/canbus.h" #include "esphome/components/canbus/canbus.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <driver/twai.h>
namespace esphome { namespace esphome {
namespace esp32_can { namespace esp32_can {
@@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus {
TickType_t tx_enqueue_timeout_ticks_{}; TickType_t tx_enqueue_timeout_ticks_{};
optional<uint32_t> tx_queue_len_{}; optional<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{}; optional<uint32_t> rx_queue_len_{};
twai_handle_t twai_handle_{nullptr};
}; };
} // namespace esp32_can } // namespace esp32_can

View File

@@ -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) { if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
return 0; 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))) { if (bytes[index] & (1 << (7 - i))) {
symbols[i] = params->bit1; symbols[i] = params->bit1;
} else { } else {

View File

@@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
return false; return false;
} }
// Generate nonce with appropriate hasher // Generate nonce - hasher must be created and used in same stack frame
bool success = false; // 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 #ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
sha256::SHA256 sha_hasher; hasher = &sha_hasher;
success = this->prepare_auth_nonce_(&sha_hasher);
} }
#endif #endif
#ifdef USE_OTA_MD5 #ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
md5::MD5Digest md5_hasher; hasher = &md5_hasher;
success = this->prepare_auth_nonce_(&md5_hasher);
} }
#endif #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<uint8_t[]>(auth_buf_size);
this->auth_buf_pos_ = 0;
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
this->log_auth_warning_(LOG_STR("Random failed"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
return false; 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 // Try to write auth_type + nonce
@@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
} }
// We have all the data, verify it // We have all the data, verify it
bool matches = false; const char *nonce = reinterpret_cast<char *>(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 #ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
sha256::SHA256 sha_hasher; hasher = &sha_hasher;
matches = this->verify_hash_auth_(&sha_hasher, hex_size);
} }
#endif #endif
#ifdef USE_OTA_MD5 #ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
md5::MD5Digest md5_hasher; hasher = &md5_hasher;
matches = this->verify_hash_auth_(&md5_hasher, hex_size);
} }
#endif #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<uint8_t[]>(auth_buf_size);
this->auth_buf_pos_ = 0;
// Generate nonce
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(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<char *>(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->init();
hasher->add(this->password_.c_str(), this->password_.length()); hasher->add(this->password_.c_str(), this->password_.length());
hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
hasher->calculate(); hasher->calculate();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #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 // Log CNonce
memcpy(log_buf, cnonce, hex_size); memcpy(log_buf, cnonce, hex_size);
log_buf[hex_size] = '\0'; log_buf[hex_size] = '\0';
@@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
#endif #endif
// Compare response // 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 { size_t ESPHomeOTAComponent::get_auth_hex_size_() const {

View File

@@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
bool handle_auth_send_(); bool handle_auth_send_();
bool handle_auth_read_(); bool handle_auth_read_();
bool select_auth_type_(); 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; size_t get_auth_hex_size_() const;
void cleanup_auth_(); void cleanup_auth_();
void log_auth_warning_(const LogString *msg); void log_auth_warning_(const LogString *msg);

View File

@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 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) \ #define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \ if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ this->log_error_and_mark_failed_(err, message); \
this->mark_failed(); \
return; \ return; \
} }
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ #define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \ if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ this->log_error_and_mark_failed_(err, message); \
this->mark_failed(); \
return ret; \ return ret; \
} }

View File

@@ -106,6 +106,7 @@ class EthernetComponent : public Component {
void start_connect_(); void start_connect_();
void finish_connect_(); void finish_connect_();
void dump_connect_params_(); void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081 #ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081. /// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); 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) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern EthernetComponent *global_eth_component; 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); extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
#endif #endif

View File

@@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() {
delay(20); // This delay guarantees the sensor will in fact be powered power. delay(20); // This delay guarantees the sensor will in fact be powered power.
if (this->check_password_()) { if (this->check_password_()) {
if (this->new_password_ != -1) { if (this->new_password_ != std::numeric_limits<uint32_t>::max()) {
if (this->set_password_()) if (this->set_password_())
return; return;
} else { } else {

View File

@@ -6,6 +6,7 @@
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include <limits>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF};
uint16_t capacity_ = 64; uint16_t capacity_ = 64;
uint32_t password_ = 0x0; uint32_t password_ = 0x0;
uint32_t new_password_ = -1; uint32_t new_password_ = std::numeric_limits<uint32_t>::max();
GPIOPin *sensing_pin_{nullptr}; GPIOPin *sensing_pin_{nullptr};
GPIOPin *sensor_power_pin_{nullptr}; GPIOPin *sensor_power_pin_{nullptr};
uint8_t enrollment_image_ = 0; uint8_t enrollment_image_ = 0;

View File

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

View File

@@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
int number_items_fit_to_screen = 0; int number_items_fit_to_screen = 0;
const int max_item_index = this->displayed_item_->items_size() - 1; 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<size_t>(max_item_index); i++) {
const auto *item = this->displayed_item_->get_item(i); const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_; const bool selected = i == this->cursor_index_;
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); 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_); display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_);
auto y_offset = bounds->y; auto y_offset = bounds->y;
for (size_t i = first_item_index; i <= last_item_index; i++) { for (size_t i = static_cast<size_t>(first_item_index);
last_item_index >= 0 && i <= static_cast<size_t>(last_item_index); i++) {
const auto *item = this->displayed_item_->get_item(i); const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_; const bool selected = i == this->cursor_index_;
display::Rect dimensions = menu_dimensions[i]; display::Rect dimensions = menu_dimensions[i];

View File

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

View File

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

View File

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

View File

@@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
size_t len = bytes_read / sizeof(int16_t); size_t len = bytes_read / sizeof(int16_t);
int16_t *tmp_buf = (int16_t *) new_data; 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]; int16_t tmp = tmp_buf[i];
tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i] = tmp_buf[i + 1];
tmp_buf[i + 1] = tmp; tmp_buf[i + 1] = tmp;

View File

@@ -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 // 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); this->write_array(ptr, w * h * 2);
} else { } else {
for (size_t y = 0; y != h; y++) { for (size_t y = 0; y != static_cast<size_t>(h); y++) {
this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); 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(); App.feed_wdt();
} }
// end of line? Skip to the next. // end of line? Skip to the next.
if (++pixel == w) { if (++pixel == static_cast<size_t>(w)) {
pixel = 0; pixel = 0;
ptr += (x_pad + x_offset) * 2; ptr += (x_pad + x_offset) * 2;
} }

View File

@@ -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) { bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(data); JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
if (doc.overflowed() || doc.isNull()) if (doc.overflowed() || doc.isNull())
return false; return false;
return f(doc.as<JsonObject>()); return f(doc.as<JsonObject>());
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // 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 // 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 #ifdef USE_PSRAM
auto doc_allocator = SpiRamAllocator(); auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator); 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!"); ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return JsonObject(); // return unbound object return JsonObject(); // return unbound object
} }
DeserializationError err = deserializeJson(json_document, data); DeserializationError err = deserializeJson(json_document, data, len);
if (err == DeserializationError::Ok) { if (err == DeserializationError::Ok) {
return json_document; return json_document;

View File

@@ -2,6 +2,7 @@
#include <vector> #include <vector>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT #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. /// 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); 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) /// 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<const uint8_t *>(data.c_str()), data.size());
}
/// Builder class for creating JSON documents without lambdas /// Builder class for creating JSON documents without lambdas
class JsonBuilder { class JsonBuilder {

View File

@@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() {
LOG_SENSOR(" ", "Flow", this->flow_sensor_); LOG_SENSOR(" ", "Flow", this->flow_sensor_);
LOG_SENSOR(" ", "Volume", this->volume_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]); LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[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 // 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]) { if (command == this->custom_commands_[i]) {
this->custom_sensors_[i]->publish_state(value); this->custom_sensors_[i]->publish_state(value);
} }

View File

@@ -13,8 +13,8 @@ class KeyCollector : public Component {
void loop() override; void loop() override;
void dump_config() override; void dump_config() override;
void set_provider(key_provider::KeyProvider *provider); void set_provider(key_provider::KeyProvider *provider);
void set_min_length(int min_length) { this->min_length_ = min_length; }; void set_min_length(uint32_t min_length) { this->min_length_ = min_length; };
void set_max_length(int max_length) { this->max_length_ = max_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_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_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; }; 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: protected:
void key_pressed_(uint8_t key); void key_pressed_(uint8_t key);
int min_length_{0}; uint32_t min_length_{0};
int max_length_{0}; uint32_t max_length_{0};
std::string start_keys_; std::string start_keys_;
std::string end_keys_; std::string end_keys_;
bool end_key_required_{false}; bool end_key_required_{false};

View File

@@ -10,11 +10,15 @@ namespace light {
static const char *const TAG = "light"; static const char *const TAG = "light";
// Helper functions to reduce code size for logging // Helper functions to reduce code size for logging
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) { float max = 1.0f) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max); 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) { static void log_feature_not_supported(const char *name, const LogString *feature) {
ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(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)); ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message));
} }
#else #else
#define log_validation_warning(name, param_name, val, min, max)
#define log_feature_not_supported(name, feature) #define log_feature_not_supported(name, feature)
#define log_color_mode_not_supported(name, feature) #define log_color_mode_not_supported(name, feature)
#define log_invalid_parameter(name, message) #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) { \ LightCall &LightCall::set_##name(type name) { \
this->name##_ = name; \ this->name##_ = name; \
this->set_flag_(flag, true); \ this->set_flag_(flag); \
return *this; \ 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_() { LightColorValues LightCall::validate_() {
auto *name = this->parent_->get_name().c_str(); auto *name = this->parent_->get_name().c_str();
auto traits = this->parent_->get_traits(); auto traits = this->parent_->get_traits();
@@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() {
// Color mode check // Color mode check
if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { 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_))); 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 // Ensure there is always a color mode set
if (!this->has_color_mode()) { if (!this->has_color_mode()) {
this->color_mode_ = this->compute_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_; auto color_mode = this->color_mode_;
// Transform calls that use non-native parameters for the current mode. // Transform calls that use non-native parameters for the current mode.
this->transform_parameters_(); this->transform_parameters_();
// Brightness exists check // Business logic adjustments before validation
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())
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect. // 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_; 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). // 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) { if (this->has_brightness() && this->brightness_ == 0.0f) {
this->state_ = false; this->state_ = false;
this->set_flag_(FLAG_HAS_STATE, true); this->set_flag_(FLAG_HAS_STATE);
this->brightness_ = 1.0f; this->brightness_ = 1.0f;
} }
// Set color brightness to 100% if currently zero and a color is set. // 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_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() &&
if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { this->parent_->remote_values.get_color_brightness() == 0.0f) {
this->color_brightness_ = 1.0f; this->color_brightness_ = 1.0f;
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); 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; auto v = this->parent_->remote_values;
if (this->has_color_mode()) if (this->has_color_mode())
v.set_color_mode(this->color_mode_); v.set_color_mode(this->color_mode_);
if (this->has_state()) if (this->has_state())
v.set_state(this->state_); v.set_state(this->state_);
if (this->has_brightness())
v.set_brightness(this->brightness_); #define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
if (this->has_color_brightness()) if (this->has_##field()) { \
v.set_color_brightness(this->color_brightness_); clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
if (this->has_red()) v.setter(this->field##_); \
v.set_red(this->red_); }
if (this->has_green())
v.set_green(this->green_); VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
if (this->has_blue()) VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
v.set_blue(this->blue_); VALIDATE_AND_APPLY(red, set_red, "Red")
if (this->has_white()) VALIDATE_AND_APPLY(green, set_green, "Green")
v.set_white(this->white_); VALIDATE_AND_APPLY(blue, set_blue, "Blue")
if (this->has_color_temperature()) VALIDATE_AND_APPLY(white, set_white, "White")
v.set_color_temperature(this->color_temperature_); VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
if (this->has_cold_white()) VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
v.set_cold_white(this->cold_white_); VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
if (this->has_warm_white()) traits.get_max_mireds())
v.set_warm_white(this->warm_white_);
#undef VALIDATE_AND_APPLY
v.normalize_color(); v.normalize_color();
// Flash length check // Flash length check
if (this->has_flash_() && this->flash_length_ == 0) { if (this->has_flash_() && this->flash_length_ == 0) {
log_invalid_parameter(name, LOG_STR("flash length must be greater than zero")); log_invalid_parameter(name, LOG_STR("flash length must be >0"));
this->set_flag_(FLAG_HAS_FLASH, false); this->clear_flag_(FLAG_HAS_FLASH);
} }
// validate transition length/flash length/effect not used at the same time // 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 effect is already active, remove effect start
if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { 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 // validate effect index
if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); 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_())) { if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash"));
this->set_flag_(FLAG_HAS_TRANSITION, false); this->clear_flag_(FLAG_HAS_TRANSITION);
this->set_flag_(FLAG_HAS_FLASH, false); this->clear_flag_(FLAG_HAS_FLASH);
} }
if (this->has_flash_() && this->has_transition_()) { if (this->has_flash_() && this->has_transition_()) {
log_invalid_parameter(name, LOG_STR("flash cannot be used with 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) && if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
supports_transition) { supports_transition) {
// nothing specified and light supports transitions, set default transition length // nothing specified and light supports transitions, set default transition length
this->transition_length_ = this->parent_->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) { if (this->has_transition_() && this->transition_length_ == 0) {
// 0 transition is interpreted as no transition (instant change) // 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) { if (this->has_transition_() && !supports_transition)
log_feature_not_supported(name, LOG_STR("transitions")); this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
// If not a flash and turning the light off, then disable the light // 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 // 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_flash_() && !target_state) {
if (this->has_effect_()) { if (this->has_effect_()) {
log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); 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) { } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
// Auto turn off effect // Auto turn off effect
this->effect_ = 0; this->effect_ = 0;
this->set_flag_(FLAG_HAS_EFFECT, true); this->set_flag_(FLAG_HAS_EFFECT);
} }
} }
// Disable saving for flashes // Disable saving for flashes
if (this->has_flash_()) if (this->has_flash_())
this->set_flag_(FLAG_SAVE, false); this->clear_flag_(FLAG_SAVE);
return v; return v;
} }
@@ -418,12 +396,12 @@ void LightCall::transform_parameters_() {
const float gamma = this->parent_->get_gamma_correct(); const float gamma = this->parent_->get_gamma_correct();
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
this->warm_white_ = gamma_uncorrect(ww_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_COLD_WHITE);
this->set_flag_(FLAG_HAS_WARM_WHITE, true); this->set_flag_(FLAG_HAS_WARM_WHITE);
} }
if (this->has_white()) { if (this->has_white()) {
this->brightness_ = this->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<std::string> effect) {
} }
LightCall &LightCall::set_effect(uint32_t effect_number) { LightCall &LightCall::set_effect(uint32_t effect_number) {
this->effect_ = effect_number; this->effect_ = effect_number;
this->set_flag_(FLAG_HAS_EFFECT, true); this->set_flag_(FLAG_HAS_EFFECT);
return *this; return *this;
} }
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) { LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {

View File

@@ -4,6 +4,10 @@
#include <set> #include <set>
namespace esphome { namespace esphome {
// Forward declaration
struct LogString;
namespace light { namespace light {
class LightState; class LightState;
@@ -207,14 +211,14 @@ class LightCall {
FLAG_SAVE = 1 << 15, FLAG_SAVE = 1 << 15,
}; };
bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag // Helper to set flag - defaults to true for common case
void set_flag_(FieldFlags flag, bool value) { void set_flag_(FieldFlags flag, bool value = true) {
if (value) { if (value) {
this->flags_ |= flag; this->flags_ |= flag;
} else { } 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_; LightState *parent_;
// Light state values - use flags_ to check if a value has been set. // Light state values - use flags_ to check if a value has been set.

View File

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -95,6 +95,7 @@ DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level" CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id" CONF_LOGGER_ID = "logger_id"
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
UART_SELECTION_ESP32 = { UART_SELECTION_ESP32 = {
@@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
cv.Optional(CONF_INITIAL_LEVEL): is_log_level, 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.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
@@ -291,8 +293,12 @@ async def to_code(config):
) )
cg.add(log.pre_setup()) cg.add(log.pre_setup())
for tag, log_level in config[CONF_LOGS].items(): # Enable runtime tag levels if logs are configured or explicitly enabled
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) 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") cg.add_define("USE_LOGGER")
this_severity = LOG_LEVEL_SEVERITY.index(level) 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]] level = LOG_LEVELS[config[CONF_LEVEL]]
logger = await cg.get_variable(config[CONF_LOGGER_ID]) logger = await cg.get_variable(config[CONF_LOGGER_ID])
if tag := config.get(CONF_TAG): if tag := config.get(CONF_TAG):
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
text = str(cg.statement(logger.set_log_level(tag, level))) text = str(cg.statement(logger.set_log_level(tag, level)))
else: else:
text = str(cg.statement(logger.set_log_level(level))) text = str(cg.statement(logger.set_log_level(level)))

View File

@@ -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 #endif // USE_STORE_LOG_STR_IN_FLASH
inline uint8_t Logger::level_for(const char *tag) { inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
auto it = this->log_levels_.find(tag); auto it = this->log_levels_.find(tag);
if (it != this->log_levels_.end()) if (it != this->log_levels_.end())
return it->second; return it->second;
#endif
return this->current_level_; 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_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) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
@@ -271,9 +275,11 @@ void Logger::dump_config() {
} }
#endif #endif
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
for (auto &it : this->log_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) { void Logger::set_log_level(uint8_t level) {

View File

@@ -36,6 +36,13 @@ struct device;
namespace esphome::logger { 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) // ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE '\0', // NONE
@@ -133,8 +140,10 @@ class Logger : public Component {
/// Set the default log level for this logger. /// Set the default log level for this logger.
void set_log_level(uint8_t level); void set_log_level(uint8_t level);
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
/// Set the log level of the specified tag. /// 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_; } uint8_t get_log_level() { return this->current_level_; }
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
@@ -242,7 +251,9 @@ class Logger : public Component {
#endif #endif
// Large objects (internally aligned) // Large objects (internally aligned)
std::map<std::string, uint8_t> log_levels_{}; #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
std::map<const char *, uint8_t, CStrCompare> log_levels_{};
#endif
CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{}; CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
CallbackManager<void(uint8_t)> level_callback_{}; CallbackManager<void(uint8_t)> level_callback_{};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
@@ -355,6 +366,12 @@ class Logger : public Component {
buffer[pos++] = '['; buffer[pos++] = '[';
copy_string(buffer, pos, tag); copy_string(buffer, pos, tag);
buffer[pos++] = ':'; 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 hundreds = line / 100;
int remainder = line - hundreds * 100; int remainder = line - hundreds * 100;
int tens = remainder / 10; int tens = remainder / 10;

View File

@@ -3,11 +3,10 @@
namespace esphome::logger { namespace esphome::logger {
void LoggerLevelSelect::publish_state(int level) { void LoggerLevelSelect::publish_state(int level) {
auto value = this->at(level); const auto &option = this->at(level_to_index(level));
if (!value) { if (!option)
return; return;
} Select::publish_state(option.value());
Select::publish_state(value.value());
} }
void LoggerLevelSelect::setup() { void LoggerLevelSelect::setup() {
@@ -16,10 +15,10 @@ void LoggerLevelSelect::setup() {
} }
void LoggerLevelSelect::control(const std::string &value) { void LoggerLevelSelect::control(const std::string &value) {
auto level = this->index_of(value); const auto index = this->index_of(value);
if (!level) if (!index)
return; return;
this->parent_->set_log_level(level.value()); this->parent_->set_log_level(index_to_level(index.value()));
} }
} // namespace esphome::logger } // namespace esphome::logger

View File

@@ -3,11 +3,18 @@
#include "esphome/components/select/select.h" #include "esphome/components/select/select.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
namespace esphome::logger { namespace esphome::logger {
class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> { class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> {
public: public:
void publish_state(int level); void publish_state(int level);
void setup() override; void setup() override;
void control(const std::string &value) 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 } // namespace esphome::logger

View File

@@ -2,6 +2,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <limits>
using esphome::i2c::ErrorCode; using esphome::i2c::ErrorCode;
@@ -28,30 +29,30 @@ bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) {
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) { template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
size_t i = 0; size_t i = 0;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i < size) { while (idx == std::numeric_limits<size_t>::max() && i < size) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i++; i++;
} }
if (idx == -1 || i + 1 >= size) if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
return val; return val;
return array[i + 1]; return array[i + 1];
} }
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1; size_t i = size - 1;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i > 0) { while (idx == std::numeric_limits<size_t>::max() && i > 0) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i--; i--;
} }
if (idx == -1 || i == 0) if (idx == std::numeric_limits<size_t>::max() || i == 0)
return val; return val;
return array[i - 1]; return array[i - 1];
} }

View File

@@ -2,6 +2,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <limits>
using esphome::i2c::ErrorCode; using esphome::i2c::ErrorCode;
@@ -14,30 +15,30 @@ static const uint8_t MAX_TRIES = 5;
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) { template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
size_t i = 0; size_t i = 0;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i < size) { while (idx == std::numeric_limits<size_t>::max() && i < size) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i++; i++;
} }
if (idx == -1 || i + 1 >= size) if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
return val; return val;
return array[i + 1]; return array[i + 1];
} }
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1; size_t i = size - 1;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i > 0) { while (idx == std::numeric_limits<size_t>::max() && i > 0) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i--; i--;
} }
if (idx == -1 || i == 0) if (idx == std::numeric_limits<size_t>::max() || i == 0)
return val; return val;
return array[i - 1]; return array[i - 1];
} }

View File

@@ -29,9 +29,9 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); }; void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); };
void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); }; void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); };
void set_keys(std::string keys) { keys_ = std::move(keys); }; void set_keys(std::string keys) { keys_ = std::move(keys); };
void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; };
void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; };
void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; };
void register_listener(MatrixKeypadListener *listener); void register_listener(MatrixKeypadListener *listener);
void register_key_trigger(MatrixKeyTrigger *trig); void register_key_trigger(MatrixKeyTrigger *trig);
@@ -40,7 +40,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
std::vector<GPIOPin *> rows_; std::vector<GPIOPin *> rows_;
std::vector<GPIOPin *> columns_; std::vector<GPIOPin *> columns_;
std::string keys_; std::string keys_;
int debounce_time_ = 0; uint32_t debounce_time_ = 0;
bool has_diodes_{false}; bool has_diodes_{false};
bool has_pulldowns_{false}; bool has_pulldowns_{false};
int pressed_key_ = -1; int pressed_key_ = -1;

View File

@@ -90,7 +90,7 @@ void MAX7219Component::loop() {
} }
if (this->scroll_mode_ == ScrollMode::STOP) { if (this->scroll_mode_ == ScrollMode::STOP) {
if (this->stepsleft_ + get_width_internal() == first_line_size + 1) { if (static_cast<size_t>(this->stepsleft_ + get_width_internal()) == first_line_size + 1) {
if (millis_since_last_scroll < this->scroll_dwell_) { 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.", 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_); this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_);

View File

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

View File

@@ -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( def mdns_service(
service: str, proto: str, port: int, txt_records: list[dict[str, str]] service: str, proto: str, port: int, txt_records: list[dict[str, str]]
): ):
return cg.StructInitializer( return cg.StructInitializer(
MDNSService, MDNSService,
("service_type", service), ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")),
("proto", proto), ("proto", cg.RawExpression(f"MDNS_STR({cg.safe_exp(proto)})")),
("port", port), ("port", port),
("txt_records", txt_records), ("txt_records", txt_records),
) )
@@ -107,23 +99,53 @@ async def to_code(config):
# Ensure at least 1 service (fallback service) # Ensure at least 1 service (fallback service)
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) 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]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
for service in config[CONF_SERVICES]: for service in config[CONF_SERVICES]:
txt = [ # Build the txt records list for the service
cg.StructInitializer( txt_records = []
MDNSTXTRecord, for txt_key, txt_value in service[CONF_TXT].items():
("key", txt_key), if cg.is_template(txt_value):
("value", await cg.templatable(txt_value, [], cg.std_string)), # It's a lambda - evaluate and store using helper
) templated_value = await cg.templatable(txt_value, [], cg.std_string)
for txt_key, txt_value in service[CONF_TXT].items() 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( exp = mdns_service(
service[CONF_SERVICE], service[CONF_SERVICE],
service[CONF_PROTOCOL], service[CONF_PROTOCOL],
await cg.templatable(service[CONF_PORT], [], cg.uint16), await cg.templatable(service[CONF_PORT], [], cg.uint16),
txt, txt_records,
) )
cg.add(var.add_extra_service(exp)) cg.add(var.add_extra_service(exp))

View File

@@ -9,24 +9,9 @@
#include <pgmspace.h> #include <pgmspace.h>
// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms // 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 #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 #else
// On non-ESP8266 platforms, use regular const char* // On non-ESP8266 platforms, use regular const char*
#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char *name = value #define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
#define MDNS_STR(name) name
#endif #endif
#ifdef USE_API #ifdef USE_API
@@ -46,30 +31,10 @@ static const char *const TAG = "mdns";
#endif #endif
// Define all constant strings using the macro // 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_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"); // Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_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");
void MDNSComponent::compile_records_() { void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name(); 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. // in mdns/__init__.py. If you add a new service here, update both locations.
#ifdef USE_API #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) { 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.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP); service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port(); service.port = api::global_api_server->get_port();
@@ -112,73 +86,92 @@ void MDNSComponent::compile_records_() {
txt_records.reserve(txt_count); txt_records.reserve(txt_count);
if (!friendly_name_empty) { 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_VERSION), MDNS_STR(VALUE_VERSION)});
txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))});
#ifdef USE_ESP8266 #ifdef USE_ESP8266
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
#elif defined(USE_LIBRETINY) #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 #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) #if defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET) #elif defined(USE_ETHERNET)
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
#elif defined(USE_OPENTHREAD) #elif defined(USE_OPENTHREAD)
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
#endif #endif
#ifdef USE_API_NOISE #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"); MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
if (api::global_api_server->get_noise_ctx()->has_psk()) { bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)}); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
} else { txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)});
}
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); 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 #endif // ESPHOME_PROJECT_NAME
#ifdef USE_DASHBOARD_IMPORT #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
} }
#endif // USE_API #endif // USE_API
#ifdef USE_PROMETHEUS #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.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT; prom_service.port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_WEBSERVER #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.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP); web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT; web_service.port = USE_WEBSERVER_PORT;
#endif #endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) #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 // 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 // This is just to have *some* mDNS service so that .local resolution works
auto &fallback_service = this->services_[this->services_.count()++]; auto &fallback_service = this->services_.emplace_next();
fallback_service.service_type = "_http"; fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = "_tcp"; fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT; 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 #endif
} }
@@ -190,11 +183,10 @@ void MDNSComponent::dump_config() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, " Services:"); ESP_LOGV(TAG, " Services:");
for (const auto &service : this->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<TemplatableValue<uint16_t> &>(service.port).value()); const_cast<TemplatableValue<uint16_t> &>(service.port).value());
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(), ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
#endif #endif

View File

@@ -9,21 +9,34 @@
namespace esphome { namespace esphome {
namespace mdns { 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<const esphome::mdns::MDNSString *>(name))
#ifdef USE_ESP8266
#include <pgmspace.h>
#define MDNS_STR_ARG(s) ((PGM_P) (s))
#else
#define MDNS_STR_ARG(s) (reinterpret_cast<const char *>(s))
#endif
// Service count is calculated at compile time by Python codegen // Service count is calculated at compile time by Python codegen
// MDNS_SERVICE_COUNT will always be defined // MDNS_SERVICE_COUNT will always be defined
struct MDNSTXTRecord { struct MDNSTXTRecord {
std::string key; const MDNSString *key;
TemplatableValue<std::string> value; const MDNSString *value;
}; };
struct MDNSService { struct MDNSService {
// service name _including_ underscore character prefix // service name _including_ underscore character prefix
// as defined in RFC6763 Section 7 // as defined in RFC6763 Section 7
std::string service_type; const MDNSString *service_type;
// second label indicating protocol _including_ underscore character prefix // second label indicating protocol _including_ underscore character prefix
// as defined in RFC6763 Section 7, like "_tcp" or "_udp" // as defined in RFC6763 Section 7, like "_tcp" or "_udp"
std::string proto; const MDNSString *proto;
TemplatableValue<uint16_t> port; TemplatableValue<uint16_t> port;
std::vector<MDNSTXTRecord> txt_records; std::vector<MDNSTXTRecord> txt_records;
}; };
@@ -39,13 +52,24 @@ class MDNSComponent : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES #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 #endif
const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; } const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
void on_shutdown() override; 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<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
protected: protected:
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
std::string hostname_; std::string hostname_;

Some files were not shown because too many files have changed in this diff Show More