1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-03 00:21:56 +00:00

Compare commits

..

7 Commits

Author SHA1 Message Date
J. Nick Koston
f2682d9df5 response suggestions 2025-10-02 15:22:21 +02:00
Jesse Hills
211a8c872b Add action response to tests 2025-10-01 13:58:19 +13:00
Jesse Hills
f4b7009c96 move callback 2025-10-01 13:50:07 +13:00
Jesse Hills
226399222d move error message 2025-10-01 11:16:07 +13:00
Jesse Hills
9a95ec95f9 Merge branch 'dev' into jesserockz-2025-457 2025-10-01 11:12:55 +13:00
Jesse Hills
2ef4f3c65f Update esphome/components/api/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-01 08:45:58 +13:00
Jesse Hills
6c362d42c3 [api] Add support for getting action responses from home-assistant 2025-09-30 15:28:41 +13:00
238 changed files with 2319 additions and 4591 deletions

View File

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

View File

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

View File

@@ -391,7 +391,7 @@ jobs:
./script/test_build_components -e compile -c ${{ matrix.file }} ./script/test_build_components -e compile -c ${{ matrix.file }}
test-build-components-splitter: test-build-components-splitter:
name: Split components for testing into 10 components per group name: Split components for testing into 20 groups maximum
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
@@ -402,10 +402,10 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Split components into groups of 10 - name: Split components into 20 groups
id: split id: split
run: | run: |
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(10) | join(" ")]') components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
echo "components=$components" >> $GITHUB_OUTPUT echo "components=$components" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
@@ -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: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
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@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
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@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
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@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.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.14.0 rev: v0.13.2
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -139,7 +139,6 @@ 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
@@ -161,6 +160,7 @@ esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz esphome/components/espnow/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/ezo_pmp/* @carlos-sarmiento
@@ -257,7 +257,6 @@ 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
@@ -430,7 +429,6 @@ 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.0b1 PROJECT_NUMBER = 2025.10.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,11 +14,9 @@ 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,
@@ -242,8 +240,6 @@ 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_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ && if (loop_count_ && 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 (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) { if (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,17 +9,14 @@ 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_RESPONSE,
CONF_ON_SUCCESS,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PORT, CONF_PORT,
CONF_REBOOT_TIMEOUT, CONF_REBOOT_TIMEOUT,
@@ -38,21 +35,9 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "api" DOMAIN = "api"
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket", "json"]
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_(
@@ -82,7 +67,7 @@ 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_SEND_QUEUE = "max_send_queue" CONF_MAX_CONNECTIONS = "max_connections"
def validate_encryption_key(value): def validate_encryption_key(value):
@@ -205,19 +190,6 @@ CONFIG_SCHEMA = cv.All(
host=8, # Abundant resources host=8, # Abundant resources
ln882x=8, # Moderate RAM ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=20), ): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=16, # Abundant resources
ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=64),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS), cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
@@ -240,7 +212,6 @@ async def to_code(config):
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
if CONF_MAX_CONNECTIONS in config: if CONF_MAX_CONNECTIONS in config:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_SERVICES if any services are enabled # Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
@@ -309,26 +280,11 @@ 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: def _validate_response_config(config):
# Validate dependencies: if CONF_RESPONSE_TEMPLATE in config and not config.get(CONF_ON_RESPONSE):
# - 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( raise cv.Invalid(
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.", f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` 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 return config
@@ -348,9 +304,14 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
{cv.string: cv.returning_lambda} {cv.string: cv.returning_lambda}
), ),
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string), cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True), {
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HomeAssistantActionResponseTrigger
),
},
single=True,
),
} }
), ),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
@@ -390,38 +351,22 @@ async def homeassistant_service_to_code(
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): 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))
if on_response := config.get(CONF_ON_RESPONSE):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS") trigger = cg.new_Pvariable(
cg.add(var.set_wants_status()) on_response[CONF_TRIGGER_ID],
await automation.build_automation( template_arg,
var.get_error_trigger(), var,
[(cg.std_string, "error"), *args], )
on_error, await automation.build_automation(
trigger,
[(cg.std_shared_ptr.template(ActionResponse), "response"), *args],
on_response,
) )
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,9 +780,8 @@ 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"]; uint32 call_id = 6; // Call ID for response tracking
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; string response_template = 7 [(no_zero_copy) = true]; // Optional Jinja template for response processing
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 sent by Home Assistant to ESPHome with service call response data
@@ -795,7 +794,7 @@ message HomeassistantActionResponse {
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false 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"]; bytes response_data = 4 [(pointer_to_buffer) = true]; // Service response data
} }
// ==================== 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,7 +116,8 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); on_fatal_error();
this->log_warning_(LOG_STR("Helper init failed"), err);
return; return;
} }
this->client_info_.peername = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
@@ -146,7 +147,8 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); on_fatal_error();
this->log_socket_operation_failed_(err);
return; return;
} }
@@ -161,13 +163,17 @@ void APIConnection::loop() {
// No more data available // No more data available
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
this->fatal_error_with_log_(LOG_STR("Reading failed"), err); on_fatal_error();
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
this->read_message(buffer.data_len, buffer.type, if (buffer.data_len > 0) {
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->flags_.remove) if (this->flags_.remove)
return; return;
} }
@@ -199,8 +205,7 @@ void APIConnection::loop() {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
this->client_info_.peername.c_str());
} }
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting // Only send ping if we're not disconnecting
@@ -250,7 +255,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // close will happen on next loop
ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
this->flags_.next_close = true; this->flags_.next_close = true;
DisconnectResponse resp; DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
@@ -1380,7 +1385,7 @@ void APIConnection::complete_authentication_() {
} }
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
#endif #endif
@@ -1389,11 +1394,6 @@ 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,17 +1550,10 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #if defined(USE_API_HOMEASSISTANT_SERVICES) && defined(USE_API_HOMEASSISTANT_ACTION_RESPONSES)
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) { void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message,
if (msg.response_data_len > 0) { reinterpret_cast<const char *>(msg.response_data), msg.response_data_len);
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 #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -1593,7 +1586,8 @@ 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) {
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); on_fatal_error();
this->log_socket_operation_failed_(err);
return false; return false;
} }
if (this->helper_->can_write_without_blocking()) if (this->helper_->can_write_without_blocking())
@@ -1612,7 +1606,8 @@ 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) {
this->fatal_error_with_log_(LOG_STR("Packet write failed"), err); on_fatal_error();
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
@@ -1621,12 +1616,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
} }
#endif #endif
void APIConnection::on_no_setup_connection() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@@ -1798,7 +1793,8 @@ 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) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); on_fatal_error();
this->log_warning_(LOG_STR("Batch write failed"), err);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1877,8 +1873,12 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES #endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_warning_(const LogString *message, APIError err) { void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); 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

View File

@@ -19,6 +19,14 @@ namespace esphome::api {
struct ClientInfo { struct ClientInfo {
std::string name; // Client name from Hello message std::string name; // Client name from Hello message
std::string peername; // IP:port from socket std::string peername; // IP:port from socket
std::string get_combined_info() const {
if (name == peername) {
// Before Hello message, both are the same
return name;
}
return name + " (" + peername + ")";
}
}; };
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
@@ -131,8 +139,8 @@ class APIConnection final : public APIServerConnection {
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif
#endif // USE_API_HOMEASSISTANT_SERVICES #endif
#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;
@@ -273,8 +281,7 @@ class APIConnection final : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const std::string &get_name() const { return this->client_info_.name; } std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
const std::string &get_peername() const { return this->client_info_.peername; }
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
@@ -735,11 +742,8 @@ 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);
// Helper to handle fatal errors with logging // Specific helper for duplicated error message
inline void fatal_error_with_log_(const LogString *message, APIError err) { void log_socket_operation_failed_(APIError err);
this->on_fatal_error();
this->log_warning_(message, err);
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -13,8 +13,7 @@ namespace esphome::api {
static const char *const TAG = "api.frame_helper"; static const char *const TAG = "api.frame_helper";
#define HELPER_LOG(msg, ...) \ #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -81,7 +80,7 @@ const LogString *api_error_to_logstr(APIError err) {
// Default implementation for loop - handles sending buffered data // Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() { APIError APIFrameHelper::loop() {
if (this->tx_buf_count_ > 0) { if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_(); APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
@@ -103,20 +102,9 @@ APIError APIFrameHelper::handle_socket_write_error_() {
// Helper method to buffer data from IOVs // Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) { uint16_t offset) {
// Check if queue is full SendBuffer buffer;
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) { buffer.size = total_write_len - offset;
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_); buffer.data = std::make_unique<uint8_t[]>(buffer.size);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset; uint16_t to_skip = offset;
uint16_t write_pos = 0; uint16_t write_pos = 0;
@@ -129,15 +117,12 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt,
// Include this segment (partially or fully) // Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip; const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip; uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer->data.get() + write_pos, src, len); std::memcpy(buffer.data.get() + write_pos, src, len);
write_pos += len; write_pos += len;
to_skip = 0; to_skip = 0;
} }
} }
this->tx_buf_.push_back(std::move(buffer));
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
} }
// This method writes data to socket or buffers it // This method writes data to socket or buffers it
@@ -155,7 +140,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
#endif #endif
// Try to send any existing buffered data first if there is any // Try to send any existing buffered data first if there is any
if (this->tx_buf_count_ > 0) { if (!this->tx_buf_.empty()) {
APIError send_result = try_send_tx_buf_(); APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it // If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
@@ -164,7 +149,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
// If there is still data in the buffer, we can't send, buffer // If there is still data in the buffer, we can't send, buffer
// the new data and return // the new data and return
if (this->tx_buf_count_ > 0) { if (!this->tx_buf_.empty()) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered return APIError::OK; // Success, data buffered
} }
@@ -192,31 +177,32 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
} }
// Common implementation for trying to send buffered data // Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method // IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
APIError APIFrameHelper::try_send_tx_buf_() { APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
while (this->tx_buf_count_ > 0) { bool tx_buf_empty = false;
while (!tx_buf_empty) {
// Get the first buffer in the queue // Get the first buffer in the queue
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get(); SendBuffer &front_buffer = this->tx_buf_.front();
// Try to send the remaining data in this buffer // Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining()); ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
if (sent == -1) { if (sent == -1) {
return this->handle_socket_write_error_(); return this->handle_socket_write_error_();
} else if (sent == 0) { } else if (sent == 0) {
// Nothing sent but not an error // Nothing sent but not an error
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) { } else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
// Partially sent, update offset // Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t // Cast to ensure no overflow issues with uint16_t
front_buffer->offset += static_cast<uint16_t>(sent); front_buffer.offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else { } else {
// Buffer completely sent, remove it from the queue // Buffer completely sent, remove it from the queue
this->tx_buf_[this->tx_buf_head_].reset(); this->tx_buf_.pop_front();
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE; // Update empty status for the loop condition
this->tx_buf_count_--; tx_buf_empty = this->tx_buf_.empty();
// Continue loop to try sending the next buffer // Continue loop to try sending the next buffer
} }
} }

View File

@@ -1,8 +1,7 @@
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <deque>
#include <limits> #include <limits>
#include <memory>
#include <span> #include <span>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -18,17 +17,6 @@ namespace esphome::api {
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Handshake messages are limited to a small size for security
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
// Data message limits vary by platform based on available memory
#ifdef USE_ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
#else
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Forward declaration // Forward declaration
struct ClientInfo; struct ClientInfo;
@@ -91,7 +79,7 @@ class APIFrameHelper {
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop(); virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() { APIError close() {
@@ -173,7 +161,7 @@ class APIFrameHelper {
}; };
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; std::deque<SendBuffer> tx_buf_;
std::vector<struct iovec> reusable_iovs_; std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
@@ -186,10 +174,7 @@ class APIFrameHelper {
State state_{State::INITIALIZE}; State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0}; uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0}; uint8_t frame_footer_size_{0};
uint8_t tx_buf_head_{0}; // 5 bytes total, 3 bytes padding
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// 8 bytes total, 0 bytes padding
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();

View File

@@ -24,8 +24,7 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif #endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#define HELPER_LOG(msg, ...) \ #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -132,16 +131,26 @@ APIError APINoiseFrameHelper::loop() {
return APIFrameHelper::loop(); return APIFrameHelper::loop();
} }
/** Read a packet into the rx_buf_. /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
* *
* @return APIError::OK if a full packet is in rx_buf_ * @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
* *
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet. * errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/ */
APIError APINoiseFrameHelper::try_read_frame_() { APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
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
@@ -168,17 +177,16 @@ APIError APINoiseFrameHelper::try_read_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];
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data if (state_ != State::DATA && msg_size > 128) {
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; // for handshake message only permit up to 128 bytes
if (msg_size > limit) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit); HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; return APIError::BAD_HANDSHAKE_PACKET_LEN;
} }
// Reserve space for body // reserve space for body
if (this->rx_buf_.size() != msg_size) { if (rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size); rx_buf_.resize(msg_size);
} }
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
@@ -196,12 +204,12 @@ APIError APINoiseFrameHelper::try_read_frame_() {
} }
} }
LOG_PACKET_RECEIVED(this->rx_buf_); LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// Clear state for next frame (rx_buf_ still contains data for caller) // consume msg
this->rx_buf_len_ = 0; rx_buf_ = {};
this->rx_header_buf_len_ = 0; rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK; return APIError::OK;
} }
@@ -223,17 +231,18 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::CLIENT_HELLO) { if (state_ == State::CLIENT_HELLO) {
// waiting for client hello // waiting for client hello
aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
// ignore contents, may be used in future for flags // ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data // Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size(); size_t old_size = prologue_.size();
this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); prologue_.resize(old_size + 2 + frame.size());
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); prologue_[old_size] = (uint8_t) (frame.size() >> 8);
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); prologue_[old_size + 1] = (uint8_t) frame.size();
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
state_ = State::SERVER_HELLO; state_ = State::SERVER_HELLO;
} }
@@ -275,23 +284,24 @@ APIError APINoiseFrameHelper::state_action_() {
int action = noise_handshakestate_get_action(handshake_); int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) { if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg // waiting for handshake msg
aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
if (this->rx_buf_.empty()) { if (frame.empty()) {
send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) { } else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); HELPER_LOG("Bad handshake error byte: %u", frame[0]);
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} }
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) { if (err != 0) {
// Special handling for MAC failure // Special handling for MAC failure
@@ -368,33 +378,35 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->state_action_(); int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return aerr; return aerr;
} }
if (this->state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
APIError decrypt_err = APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK) { if (decrypt_err != APIError::OK)
return decrypt_err; return decrypt_err;
}
uint16_t msg_size = mbuf.size; uint16_t msg_size = mbuf.size;
uint8_t *msg_data = this->rx_buf_.data(); uint8_t *msg_data = frame.data();
if (msg_size < 4) { if (msg_size < 4) {
this->state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size); HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
@@ -402,12 +414,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) { if (data_len > msg_size - 4) {
this->state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
buffer->container = std::move(this->rx_buf_); buffer->container = std::move(frame);
buffer->data_offset = 4; buffer->data_offset = 4;
buffer->data_len = data_len; buffer->data_len = data_len;
buffer->type = type; buffer->type = type;

View File

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

View File

@@ -18,8 +18,7 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext"; static const char *const TAG = "api.plaintext";
#define HELPER_LOG(msg, ...) \ #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -47,13 +46,21 @@ APIError APIPlaintextFrameHelper::loop() {
return APIFrameHelper::loop(); return APIFrameHelper::loop();
} }
/** Read a packet into the rx_buf_. /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @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_() { APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *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
@@ -115,10 +122,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
continue; continue;
} }
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) { if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
MAX_MESSAGE_SIZE); std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
rx_header_parsed_len_ = msg_size_varint->as_uint16(); rx_header_parsed_len_ = msg_size_varint->as_uint16();
@@ -142,9 +149,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
} }
// header reading done // header reading done
// Reserve space for body // reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) { if (rx_buf_.size() != rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_); rx_buf_.resize(rx_header_parsed_len_);
} }
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
@@ -162,22 +169,24 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
} }
} }
LOG_PACKET_RECEIVED(this->rx_buf_); LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// Clear state for next frame (rx_buf_ still contains data for caller) // consume msg
this->rx_buf_len_ = 0; rx_buf_ = {};
this->rx_header_buf_pos_ = 0; rx_buf_len_ = 0;
this->rx_header_parsed_ = false; rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) { APIError aerr;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
APIError aerr = this->try_read_frame_(); std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) { if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't // Make sure to tell the remote that we don't
@@ -210,10 +219,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr; return aerr;
} }
buffer->container = std::move(this->rx_buf_); buffer->container = std::move(frame);
buffer->data_offset = 0; buffer->data_offset = 0;
buffer->data_len = this->rx_header_parsed_len_; buffer->data_len = rx_header_parsed_len_;
buffer->type = this->rx_header_parsed_type_; buffer->type = rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {

View File

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

View File

@@ -884,15 +884,8 @@ 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); buffer.encode_uint32(6, this->call_id);
#endif buffer.encode_string(7, this->response_template);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_bool(7, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_string(8, this->response_template);
#endif
} }
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->service_ref_.size()); size.add_length(1, this->service_ref_.size());
@@ -900,15 +893,8 @@ 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); 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()); size.add_length(1, this->response_template.size());
#endif
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -930,14 +916,12 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe
case 3: case 3:
this->error_message = value.as_string(); this->error_message = value.as_string();
break; break;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
case 4: { case 4: {
// Use raw data directly to avoid allocation // Use raw data directly to avoid allocation
this->response_data = value.data(); this->response_data = value.data();
this->response_data_len = value.size(); this->response_data_len = value.size();
break; break;
} }
#endif
default: default:
return false; return false;
} }

View File

@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
class HomeassistantActionRequest final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 128; static constexpr uint8_t ESTIMATED_SIZE = 126;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_request"; } const char *message_name() const override { return "homeassistant_action_request"; }
#endif #endif
@@ -1114,15 +1114,8 @@ 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}; 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{}; 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
@@ -1143,10 +1136,8 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
uint32_t call_id{0}; uint32_t call_id{0};
bool success{false}; bool success{false};
std::string error_message{}; std::string error_message{};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr}; const uint8_t *response_data{nullptr};
uint16_t response_data_len{0}; uint16_t response_data_len{0};
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif

View File

@@ -1122,15 +1122,8 @@ 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); 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); dump_field(out, "response_template", this->response_template);
#endif
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -1139,11 +1132,9 @@ void HomeassistantActionResponse::dump_to(std::string &out) const {
dump_field(out, "call_id", this->call_id); dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success); dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message); dump_field(out, "error_message", this->error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
out.append(" response_data: "); out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len)); out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n"); out.append("\n");
#endif
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -181,8 +181,7 @@ void APIServer::loop() {
// Network is down - disconnect all clients // Network is down - disconnect all clients
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->on_fatal_error(); client->on_fatal_error();
ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
client->client_info_.peername.c_str());
} }
// Continue to process and clean up the clients below // Continue to process and clean up the clients below
} }
@@ -404,38 +403,28 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
client->send_homeassistant_action(call); client->send_homeassistant_action(call);
} }
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) { void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
this->action_response_callbacks_.push_back({call_id, std::move(callback)}); this->action_response_callbacks_[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, 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) { const char *response_data, size_t response_data_len) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { auto it = this->action_response_callbacks_.find(call_id);
if (it->call_id == call_id) { if (it != this->action_response_callbacks_.end()) {
auto callback = std::move(it->callback); // Create the response object
this->action_response_callbacks_.erase(it); auto response = std::make_shared<class ActionResponse>(success, error_message, response_data, response_data_len);
ActionResponse response(success, error_message, response_data, response_data_len);
callback(response); // Call the callback
return; it->second(response);
}
// Remove the callback as it's one-time use
this->action_response_callbacks_.erase(it);
} }
} }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif
#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

@@ -111,18 +111,14 @@ class APIServer : public Component, public Controller {
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Action response handling // Action response handling
using ActionResponseCallback = std::function<void(const class ActionResponse &)>; using ActionResponseCallback = std::function<void(std::shared_ptr<class ActionResponse>)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback); 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, 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); const char *response_data, size_t response_data_len);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif
#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
@@ -199,11 +195,7 @@ class APIServer : public Component, public Controller {
std::vector<UserServiceDescriptor *> user_services_; std::vector<UserServiceDescriptor *> user_services_;
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse { std::map<uint32_t, ActionResponseCallback> action_response_callbacks_;
uint32_t call_id;
ActionResponseCallback callback;
};
std::vector<PendingActionResponse> action_response_callbacks_;
#endif #endif
// Group smaller types together // Group smaller types together

View File

@@ -7,9 +7,7 @@
#include <utility> #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" #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"
@@ -53,43 +51,44 @@ template<typename... Ts> class TemplatableKeyValuePair {
// Represents the response data from a Home Assistant action // Represents the response data from a Home Assistant action
class ActionResponse { class ActionResponse {
public: public:
ActionResponse(bool success, std::string error_message = "") ActionResponse(bool success, std::string error_message, const char *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)) {} : success_(success), error_message_(std::move(error_message)), data_(data), data_len_(data_len) {}
#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_; } bool is_success() const { return this->success_; }
const std::string &get_error_message() const { return this->error_message_; } const std::string &get_error_message() const { return this->error_message_; }
const char *get_data() const { return this->data_; }
size_t get_data_len() const { return this->data_len_; }
// Get data as parsed JSON object
// Returns unbound JsonObject if data is empty or invalid JSON
JsonObject get_json() {
if (this->data_len_ == 0)
return JsonObject(); // Return unbound JsonObject if no data
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON if (!this->parsed_json_) {
// Get data as parsed JSON object (const version returns read-only view) this->json_document_ = json::parse_json(this->data_, this->data_len_);
JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); } this->json_ = this->json_document_.as<JsonObject>();
#endif this->parsed_json_ = true;
}
return this->json_;
}
protected: protected:
bool success_; bool success_;
std::string error_message_; std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON const char *data_;
size_t data_len_;
JsonDocument json_document_; JsonDocument json_document_;
#endif JsonObject json_;
bool parsed_json_{false};
}; };
// Callback type for action responses // Callback type for action responses
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>; template<typename... Ts> using ActionResponseCallback = std::function<void(std::shared_ptr<ActionResponse>, Ts...)>;
#endif #endif
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> { template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
public: public:
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) { explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
this->flags_.is_event = is_event;
}
template<typename T> void set_service(T service) { this->service_ = service; } template<typename T> void set_service(T service) { this->service_ = service; }
@@ -107,26 +106,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) { template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template; this->response_template_ = response_template;
this->flags_.has_response_template = true; this->has_response_template_ = true;
} }
void set_wants_status() { this->flags_.wants_status = true; } void set_response_callback(ActionResponseCallback<Ts...> callback) {
void set_wants_response() { this->flags_.wants_response = true; } this->wants_response_ = true;
this->response_callback_ = callback;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
} }
#endif #endif
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event; resp.is_event = this->is_event_;
for (auto &it : this->data_) { for (auto &it : this->data_) {
resp.data.emplace_back(); resp.data.emplace_back();
auto &kv = resp.data.back(); auto &kv = resp.data.back();
@@ -147,40 +140,21 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->flags_.wants_status) { if (this->wants_response_) {
// Generate a unique call ID for this service call // Generate a unique call ID for this service call
static uint32_t call_id_counter = 1; static uint32_t call_id_counter = 1;
uint32_t call_id = call_id_counter++; uint32_t call_id = call_id_counter++;
resp.call_id = call_id; resp.call_id = call_id;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON // Set response template if provided
if (this->flags_.wants_response) { if (this->has_response_template_) {
resp.wants_response = true; std::string response_template_value = this->response_template_.value(x...);
// Set response template if provided resp.response_template = response_template_value;
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...); auto captured_args = std::make_tuple(x...);
this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) { this->parent_->register_action_response_callback(call_id, [this, captured_args](
std::apply( std::shared_ptr<ActionResponse> response) {
[this, &response](auto &&...args) { std::apply([this, &response](auto &&...args) { this->response_callback_(response, args...); }, captured_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 #endif
@@ -190,29 +164,30 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
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
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""}; TemplatableStringValue<Ts...> response_template_{""};
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>(); ActionResponseCallback<Ts...> response_callback_;
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON bool wants_response_{false};
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>(); bool has_response_template_{false};
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>(); #endif
#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 #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename... Ts>
class HomeAssistantActionResponseTrigger : public Trigger<std::shared_ptr<ActionResponse>, Ts...> {
public:
HomeAssistantActionResponseTrigger(HomeAssistantServiceCallAction<Ts...> *action) {
action->set_response_callback(
[this](std::shared_ptr<ActionResponse> response, Ts... x) { this->trigger(response, x...); });
}
};
#endif
} // namespace esphome::api
#endif #endif
#endif #endif

View File

@@ -35,7 +35,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
msg.set_name(StringRef(this->name_)); msg.set_name(StringRef(this->name_));
msg.key = this->key_; msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
for (size_t i = 0; i < sizeof...(Ts); i++) { for (int i = 0; i < sizeof...(Ts); i++) {
msg.args.emplace_back(); msg.args.emplace_back();
auto &arg = msg.args.back(); auto &arg = msg.args.back();
arg.type = arg_types[i]; arg.type = arg_types[i];
@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
protected: protected:
virtual void execute(Ts... x) = 0; virtual void execute(Ts... x) = 0;
template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) { template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...); this->execute((get_execute_arg_value<Ts>(args[S]))...);
} }

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", "2.0.1") cg.add_library("esphome/esp-audio-libs", "1.1.4")

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 (size_t i = 0; i < samples_to_scale; i++) { for (int 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) {
// Serrious error reading FLAC header, there is no recovery return FileDecoderState::POTENTIALLY_FAILED;
}
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(this->input_transfer_buffer_->get_buffer_start(), auto result = this->flac_decoder_->decode_frame(
this->input_transfer_buffer_->available(), this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
this->output_transfer_buffer_->get_buffer_end(), &output_samples); reinterpret_cast<int16_t *>(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 (size_t i = 0; i < this->action_queue_.size(); i++) { for (int 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[%zu]", i); ESP_LOGI(TAG, "HandleActionCallback[%d]", i);
(this->*ptr_func)(); (this->*ptr_func)();
} }
} }

View File

@@ -51,7 +51,7 @@ void BL0942::loop() {
if (!avail) { if (!avail) {
return; return;
} }
if (static_cast<size_t>(avail) < sizeof(buffer)) { if (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 (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode) if (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.consume_connection_slots(1, "ble_client"), esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
) )
CONF_BLE_CLIENT_ID = "ble_client_id" CONF_BLE_CLIENT_ID = "ble_client_id"

View File

@@ -42,7 +42,9 @@ 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.consume_connection_slots(connection_slots, "bluetooth_proxy")(config) esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
config
)
return { return {
**config, **config,
@@ -63,11 +65,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.IDF_MAX_CONNECTIONS), cv.Range(min=1, max=esp32_ble_tracker.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.IDF_MAX_CONNECTIONS), cv.Length(min=1, max=esp32_ble_tracker.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(ESPHOME_F("application/json")); AsyncResponseStream *stream = request->beginResponseStream(F("application/json"));
stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate"));
#ifdef USE_ESP8266 #ifdef USE_ESP8266
stream->print(ESPHOME_F("{\"mac\":\"")); stream->print(F("{\"mac\":\""));
stream->print(get_mac_address_pretty().c_str()); stream->print(get_mac_address_pretty().c_str());
stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(F("\",\"name\":\""));
stream->print(App.get_name().c_str()); stream->print(App.get_name().c_str());
stream->print(ESPHOME_F("\",\"aps\":[{}")); stream->print(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(ESPHOME_F(",{\"ssid\":\"")); stream->print(F(",{\"ssid\":\""));
stream->print(scan.get_ssid().c_str()); stream->print(scan.get_ssid().c_str());
stream->print(ESPHOME_F("\",\"rssi\":")); stream->print(F("\",\"rssi\":"));
stream->print(scan.get_rssi()); stream->print(scan.get_rssi());
stream->print(ESPHOME_F(",\"lock\":")); stream->print(F(",\"lock\":"));
stream->print(scan.get_with_auth()); stream->print(scan.get_with_auth());
stream->print(ESPHOME_F("}")); stream->print(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(ESPHOME_F("]}")); stream->print(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(ESPHOME_F("/?save")); request->redirect(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, ESPHOME_F("*"), ip); this->dns_server_->start(53, 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() == ESPHOME_F("/config.json")) { if (req->url() == F("/config.json")) {
this->handle_config(req); this->handle_config(req);
return; return;
} else if (req->url() == ESPHOME_F("/wifisave")) { } else if (req->url() == 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, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
#else #else
auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
#endif #endif
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); response->addHeader(F("Content-Encoding"), 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 (size_t i = 0; i < len - 1; i++) { for (int i = 0; i < len - 1; i++) {
crc -= response[i]; crc -= response[i];
} }
return crc; return crc;

View File

@@ -11,7 +11,7 @@ void CopyLock::setup() {
traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_assumed_state(source_->traits.get_assumed_state());
traits.set_requires_code(source_->traits.get_requires_code()); traits.set_requires_code(source_->traits.get_requires_code());
traits.set_supported_states_mask(source_->traits.get_supported_states_mask()); traits.set_supported_states(source_->traits.get_supported_states());
traits.set_supports_open(source_->traits.get_supports_open()); traits.set_supports_open(source_->traits.get_supports_open());
this->publish_state(source_->state); this->publish_state(source_->state);

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 (size_t i = 0; i < sizeof(remote_header) - 1; i++) { for (int 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 (size_t i = 0; i < sizeof(remote_header) - 1; i++) { for (int 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;
size_t bytes_count = data.size() / 2 / 8; int 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 < static_cast<size_t>(data.size()); j++) { for (size_t j = 0; j < 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,26 +380,19 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
char type_ch = ' '; char type_ch = ' ';
// debug_tolerance = 25% // debug_tolerance = 25%
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
type_ch = 'P'; type_ch = 'P';
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
type_ch = 'a'; type_ch = 'a';
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
type_ch = 'H'; type_ch = 'H';
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
type_ch = 'h'; type_ch = 'h';
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
type_ch = 'B'; type_ch = 'B';
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
type_ch = '1'; type_ch = '1';
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] && if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))
-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) {
@@ -407,7 +400,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 + 1 == static_cast<size_t>(data.size())) { if (j == data.size() - 1) {
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

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

View File

@@ -1,80 +0,0 @@
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

@@ -1,227 +0,0 @@
#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

@@ -1,93 +0,0 @@
#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

@@ -1,42 +0,0 @@
#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

@@ -1,45 +0,0 @@
#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

@@ -1,135 +0,0 @@
#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

@@ -1,23 +0,0 @@
#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_() {
uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
int coeff = -1; int coeff = -1;
for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { for (int 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 = static_cast<int>(i); coeff = i;
} }
if (coeff >= 0) { if (coeff >= 0) {

View File

@@ -296,9 +296,14 @@ 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(ver: cv.Version, release: str) -> str: def _format_framework_espidf_version(
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to ver: cv.Version, release: str, for_platformio: bool
) -> 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"
@@ -312,114 +317,157 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# 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
ARDUINO_FRAMEWORK_VERSION_LOOKUP = { RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
"recommended": cv.Version(3, 2, 1), # The platform-espressif32 version to use for arduino frameworks
"latest": cv.Version(3, 3, 1), # - https://github.com/pioarduino/platform-espressif32/releases
"dev": cv.Version(3, 3, 1), ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
}
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
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
"recommended": cv.Version(5, 4, 2), RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
"latest": cv.Version(5, 5, 1), # The platformio/espressif32 version to use for esp-idf frameworks
"dev": cv.Version(5, 5, 1), # - https://github.com/platformio/platform-espressif32/releases
} # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
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),
}
# The platform-espressif32 version # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
# - https://github.com/pioarduino/platform-espressif32/releases SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
PLATFORM_VERSION_LOOKUP = { cv.Version(5, 3, 1),
"recommended": cv.Version(54, 3, 21, "2"), cv.Version(5, 3, 0),
"latest": cv.Version(55, 3, 31), cv.Version(5, 2, 2),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop", cv.Version(5, 2, 1),
} cv.Version(5, 1, 2),
cv.Version(5, 1, 1),
cv.Version(5, 1, 0),
cv.Version(5, 0, 2),
cv.Version(5, 0, 1),
cv.Version(5, 0, 0),
]
# pioarduino versions that don't require a release number
# List based on https://github.com/pioarduino/esp-idf/releases
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
cv.Version(5, 5, 1),
cv.Version(5, 5, 0),
cv.Version(5, 4, 2),
cv.Version(5, 4, 1),
cv.Version(5, 4, 0),
cv.Version(5, 3, 3),
cv.Version(5, 3, 2),
cv.Version(5, 3, 1),
cv.Version(5, 3, 0),
cv.Version(5, 1, 5),
cv.Version(5, 1, 6),
]
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 PLATFORM_VERSION_LOOKUP: if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: if CONF_SOURCE in value:
raise cv.Invalid( raise cv.Invalid(
"Version needs to be explicitly set when a custom source or platform_version is used." "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."
) )
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]] return value
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
if value[CONF_TYPE] == FRAMEWORK_ARDUINO: lookups = {
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
else: "latest": (cv.Version(5, 2, 2), None),
version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
}
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
)
version, source = lookups[value[CONF_VERSION]]
else: 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_TYPE] == FRAMEWORK_ARDUINO: if value[CONF_SOURCE].startswith("http"):
if version < cv.Version(3, 0, 0): # prefix is necessary or platformio will complain with a cryptic error
raise cv.Invalid("Only Arduino 3.0+ is supported.") value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}"
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 CONF_PLATFORM_VERSION not in value: if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION:
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 framework version is not the recommended one. " "The selected ESP-IDF 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."
) )
@@ -429,14 +477,26 @@ 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))
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" if ver.major >= 50: # a pioarduino version
if ver.extra: release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
release += f"-{ver.extra}" if ver.extra:
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" release += f"-{ver.extra}"
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
# if platform version is a valid version constraint, prefix the default package
cv.platformio_version_constraint(value)
return f"platformio/espressif32@{value}"
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)
@@ -748,8 +808,6 @@ 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")
@@ -792,6 +850,8 @@ 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,8 +1,5 @@
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
@@ -12,19 +9,16 @@ 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 CORE, TimePeriod from esphome.core import 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.
@@ -133,28 +127,6 @@ 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")
@@ -211,9 +183,6 @@ 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)
@@ -261,56 +230,6 @@ 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:
@@ -326,10 +245,6 @@ 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)
@@ -340,26 +255,6 @@ 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
@@ -375,10 +270,6 @@ 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

@@ -213,17 +213,15 @@ 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 += "-"; name += "-" + get_mac_address().substr(6);
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()) {
// Keep first 13 chars and last 7 chars (MAC suffix), remove middle name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address
name.erase(13, name.length() - 20);
} else { } else {
name.resize(20); name = name.substr(0, 20);
} }
} }
} }

View File

@@ -152,7 +152,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 (static_cast<size_t>(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) { if (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

@@ -42,18 +42,32 @@ 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) {
// 16-bit UUID as 4-character hex string ret.uuid_.len = ESP_UUID_LEN_16;
auto parsed = parse_hex<uint16_t>(data); ret.uuid_.uuid.uuid16 = 0;
if (parsed.has_value()) { for (uint i = 0; i < data.length(); i += 2) {
ret.uuid_.len = ESP_UUID_LEN_16; uint8_t msb = data.c_str()[i];
ret.uuid_.uuid.uuid16 = parsed.value(); uint8_t lsb = data.c_str()[i + 1];
uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0;
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
} }
} else if (data.length() == 8) { } else if (data.length() == 8) {
// 32-bit UUID as 8-character hex string ret.uuid_.len = ESP_UUID_LEN_32;
auto parsed = parse_hex<uint32_t>(data); ret.uuid_.uuid.uuid32 = 0;
if (parsed.has_value()) { for (uint i = 0; i < data.length(); i += 2) {
ret.uuid_.len = ESP_UUID_LEN_32; uint8_t msb = data.c_str()[i];
ret.uuid_.uuid.uuid32 = parsed.value(); uint8_t lsb = data.c_str()[i + 1];
uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0;
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
} }
} 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)
@@ -131,16 +145,28 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
if (this->uuid_.len == uuid.uuid_.len) { if (this->uuid_.len == uuid.uuid_.len) {
switch (this->uuid_.len) { switch (this->uuid_.len) {
case ESP_UUID_LEN_16: case ESP_UUID_LEN_16:
return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16; if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) {
return true;
}
break;
case ESP_UUID_LEN_32: case ESP_UUID_LEN_32:
return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32; if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) {
return true;
}
break;
case ESP_UUID_LEN_128: case ESP_UUID_LEN_128:
return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0; for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) {
default: if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
return false; return false;
}
}
return true;
break;
} }
} else {
return this->as_128bit() == uuid.as_128bit();
} }
return this->as_128bit() == uuid.as_128bit(); return false;
} }
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const { std::string ESPBTUUID::to_string() const {

View File

@@ -26,7 +26,7 @@ from esphome.const import (
from esphome.core import CORE from esphome.core import CORE
from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble", "bytebuffer"] AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
DOMAIN = "esp32_ble_server" DOMAIN = "esp32_ble_server"

View File

@@ -49,11 +49,7 @@ void BLECharacteristic::notify() {
this->service_->get_server()->get_connected_client_count() == 0) this->service_->get_server()->get_connected_client_count() == 0)
return; return;
const uint16_t *clients = this->service_->get_server()->get_clients(); for (auto &client : 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);
@@ -77,7 +73,7 @@ void BLECharacteristic::notify() {
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
// If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on_write([this](std::span<const uint8_t> value, uint16_t conn_id) { descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) {
if (value.size() != 2) if (value.size() != 2)
return; return;
uint16_t cccd = encode_uint16(value[1], value[0]); uint16_t cccd = encode_uint16(value[1], value[0]);
@@ -125,49 +121,69 @@ 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_) {
if (!descriptor->is_created()) created &= descriptor->is_created();
return false;
} }
// All descriptors are created if we reach here if (created)
this->state_ = CREATED; this->state_ = CREATED;
return true; return this->state_ == CREATED;
} }
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_) {
if (descriptor->is_failed()) { failed |= 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) {
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value); if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
}
} }
void BLECharacteristic::set_indicate_property(bool value) { void BLECharacteristic::set_indicate_property(bool value) {
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value); if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
}
} }
void BLECharacteristic::set_notify_property(bool value) { void BLECharacteristic::set_notify_property(bool value) {
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value); if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
}
}
void BLECharacteristic::set_read_property(bool value) {
if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
}
}
void BLECharacteristic::set_write_property(bool value) {
if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
}
} }
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) {
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value); if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
}
} }
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,
@@ -192,9 +208,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (!param->read.need_rsp) if (!param->read.need_rsp)
break; // For some reason you can request a read but not want a response break; // For some reason you can request a read but not want a response
if (this->on_read_callback_) { this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ,
(*this->on_read_callback_)(param->read.conn_id); param->read.conn_id);
}
uint16_t max_offset = 22; uint16_t max_offset = 22;
@@ -262,9 +277,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
if (!param->write.is_prep) { if (!param->write.is_prep) {
if (this->on_write_callback_) { this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
(*this->on_write_callback_)(this->value_, param->write.conn_id); BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id);
}
} }
break; break;
@@ -275,9 +289,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break; break;
this->write_event_ = false; this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
if (this->on_write_callback_) { this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
(*this->on_write_callback_)(this->value_, param->exec_write.conn_id); BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id);
}
} }
esp_err_t err = esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);

View File

@@ -2,12 +2,10 @@
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector> #include <vector>
#include <span>
#include <functional>
#include <memory>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -24,10 +22,22 @@ namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
using namespace event_emitter;
class BLEService; class BLEService;
class BLECharacteristic { namespace BLECharacteristicEvt {
enum VectorEvt {
ON_WRITE,
};
enum EmptyEvt {
ON_READ,
};
} // namespace BLECharacteristicEvt
class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>,
public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> {
public: public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties); BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic(); ~BLECharacteristic();
@@ -66,15 +76,6 @@ class BLECharacteristic {
bool is_created(); bool is_created();
bool is_failed(); bool is_failed();
// Direct callback registration - only allocates when callback is set
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ =
std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
}
void on_read(std::function<void(uint16_t)> &&callback) {
this->on_read_callback_ = std::make_unique<std::function<void(uint16_t)>>(std::move(callback));
}
protected: protected:
bool write_event_{false}; bool write_event_{false};
BLEService *service_{}; BLEService *service_{};
@@ -97,11 +98,6 @@ 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(uint16_t)>> on_read_callback_;
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
enum State : uint8_t { enum State : uint8_t {

View File

@@ -74,10 +74,9 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break; break;
this->value_.attr_len = param->write.len; this->value_.attr_len = param->write.len;
memcpy(this->value_.attr_value, param->write.value, param->write.len); memcpy(this->value_.attr_value, param->write.value, param->write.len);
if (this->on_write_callback_) { this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE,
(*this->on_write_callback_)(std::span<const uint8_t>(param->write.value, param->write.len), std::vector<uint8_t>(param->write.value, param->write.value + param->write.len),
param->write.conn_id); param->write.conn_id);
}
break; break;
} }
default: default:

View File

@@ -1,26 +1,30 @@
#pragma once #pragma once
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gatt_defs.h> #include <esp_gatt_defs.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
#include <span>
#include <functional>
#include <memory>
namespace esphome { namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic; class BLECharacteristic;
// Base class for BLE descriptors namespace BLEDescriptorEvt {
class BLEDescriptor { enum VectorEvt {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
public: public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor(); virtual ~BLEDescriptor();
@@ -35,12 +39,6 @@ class BLEDescriptor {
bool is_created() { return this->state_ == CREATED; } bool is_created() { return this->state_ == CREATED; }
bool is_failed() { return this->state_ == FAILED; } bool is_failed() { return this->state_ == FAILED; }
// Direct callback registration - only allocates when callback is set
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ =
std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
}
protected: protected:
BLECharacteristic *characteristic_{nullptr}; BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_; ESPBTUUID uuid_;
@@ -48,8 +46,6 @@ class BLEDescriptor {
esp_attr_value_t value_{}; esp_attr_value_t value_{};
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
esp_gatt_perm_t permissions_{}; esp_gatt_perm_t permissions_{};
enum State : uint8_t { enum State : uint8_t {

View File

@@ -147,28 +147,20 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
return nullptr; return nullptr;
} }
void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) {
for (auto &entry : this->callbacks_) {
if (entry.type == type) {
entry.callback(conn_id);
}
}
}
void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTS_CONNECT_EVT: { case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected"); ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id); this->add_client_(param->connect.conn_id);
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id);
break; break;
} }
case ESP_GATTS_DISCONNECT_EVT: { case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected"); ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id); this->remove_client_(param->disconnect.conn_id);
this->parent_->advertising_start(); this->parent_->advertising_start();
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id);
break; break;
} }
case ESP_GATTS_REG_EVT: { case ESP_GATTS_REG_EVT: {
@@ -185,38 +177,9 @@ 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->client_count_ = 0; this->clients_.clear();
// 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,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <functional> #include <unordered_set>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -24,7 +24,18 @@ namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> { namespace BLEServerEvt {
enum EmptyEvt {
ON_CONNECT,
ON_DISCONNECT,
};
} // namespace BLEServerEvt
class BLEServer : public Component,
public GATTsEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE>,
public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> {
public: public:
void setup() override; void setup() override;
void loop() override; void loop() override;
@@ -46,34 +57,15 @@ 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->client_count_; } uint32_t get_connected_client_count() { return this->clients_.size(); }
const uint16_t *get_clients() const { return this->clients_; } const std::unordered_set<uint16_t> &get_clients() { 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;
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
// Direct callback registration - supports multiple callbacks
void on_connect(std::function<void(uint16_t)> &&callback) {
this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)});
}
void on_disconnect(std::function<void(uint16_t)> &&callback) {
this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)});
}
protected: protected:
enum class CallbackType : uint8_t {
ON_CONNECT,
ON_DISCONNECT,
};
struct CallbackEntry {
CallbackType type;
std::function<void(uint16_t)> callback;
};
struct ServiceEntry { struct ServiceEntry {
ESPBTUUID uuid; ESPBTUUID uuid;
uint8_t inst_id; uint8_t inst_id;
@@ -82,19 +74,14 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void restart_advertising_(); void restart_advertising_();
int8_t find_client_index_(uint16_t conn_id) const; void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
void add_client_(uint16_t conn_id); void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
void remove_client_(uint16_t conn_id);
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
std::vector<CallbackEntry> callbacks_;
std::vector<uint8_t> manufacturer_data_{}; std::vector<uint8_t> manufacturer_data_{};
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};
bool registered_{false}; bool registered_{false};
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; std::unordered_set<uint16_t> clients_;
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

@@ -14,10 +14,9 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
BLECharacteristic *characteristic) { BLECharacteristic *characteristic) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
// Convert span to vector for trigger BLECharacteristicEvt::VectorEvt::ON_WRITE,
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); [on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
});
return on_write_trigger; return on_write_trigger;
} }
#endif #endif
@@ -26,10 +25,9 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { descriptor->on(
// Convert span to vector for trigger BLEDescriptorEvt::VectorEvt::ON_WRITE,
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); [on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
});
return on_write_trigger; return on_write_trigger;
} }
#endif #endif
@@ -37,7 +35,8 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT #ifdef USE_ESP32_BLE_SERVER_ON_CONNECT
Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory) Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); server->on(BLEServerEvt::EmptyEvt::ON_CONNECT,
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger; return on_connect_trigger;
} }
#endif #endif
@@ -45,22 +44,38 @@ Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *serv
#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT #ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT
Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory) Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger; return on_disconnect_trigger;
} }
#endif #endif
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) { const std::function<void()> &pre_notify_listener) {
// Find and remove existing listener for this characteristic // Find and remove existing listener for this characteristic
auto *existing = this->find_listener_(characteristic); auto *existing = this->find_listener_(characteristic);
if (existing != nullptr) { if (existing != nullptr) {
// Remove the previous listener
characteristic->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::off(BLECharacteristicEvt::EmptyEvt::ON_READ,
existing->listener_id);
// Remove the pre-notify listener
this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, existing->pre_notify_listener_id);
// Remove from vector // Remove from vector
this->remove_listener_(characteristic); this->remove_listener_(characteristic);
} }
// Create a new listener for the pre-notify event
EventEmitterListenerID pre_notify_listener_id =
this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY,
[pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) {
// Only call the pre-notify listener if the characteristic is the one we are interested in
if (characteristic == evt_characteristic) {
pre_notify_listener();
}
});
// Save the entry to the vector // Save the entry to the vector
this->listeners_.push_back({characteristic, pre_notify_listener}); this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id});
} }
BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_( BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_(

View File

@@ -4,6 +4,7 @@
#include "ble_characteristic.h" #include "ble_characteristic.h"
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include <vector> #include <vector>
@@ -17,6 +18,10 @@ namespace esp32_ble_server {
namespace esp32_ble_server_automations { namespace esp32_ble_server_automations {
using namespace esp32_ble; using namespace esp32_ble;
using namespace event_emitter;
// Invalid listener ID constant - 0 is used as sentinel value in EventEmitter
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
class BLETriggers { class BLETriggers {
public: public:
@@ -36,29 +41,38 @@ class BLETriggers {
}; };
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
enum BLECharacteristicSetValueActionEvt {
PRE_NOTIFY,
};
// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic // Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic
class BLECharacteristicSetValueActionManager { class BLECharacteristicSetValueActionManager
: public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
public: public:
// Singleton pattern // Singleton pattern
static BLECharacteristicSetValueActionManager *get_instance() { static BLECharacteristicSetValueActionManager *get_instance() {
static BLECharacteristicSetValueActionManager instance; static BLECharacteristicSetValueActionManager instance;
return &instance; return &instance;
} }
void set_listener(BLECharacteristic *characteristic, const std::function<void()> &pre_notify_listener); void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id,
bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; } const std::function<void()> &pre_notify_listener);
void emit_pre_notify(BLECharacteristic *characteristic) { EventEmitterListenerID get_listener(BLECharacteristic *characteristic) {
for (const auto &entry : this->listeners_) { for (const auto &entry : this->listeners_) {
if (entry.characteristic == characteristic) { if (entry.characteristic == characteristic) {
entry.pre_notify_listener(); return entry.listener_id;
break;
} }
} }
return INVALID_LISTENER_ID;
}
void emit_pre_notify(BLECharacteristic *characteristic) {
this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic);
} }
private: private:
struct ListenerEntry { struct ListenerEntry {
BLECharacteristic *characteristic; BLECharacteristic *characteristic;
std::function<void()> pre_notify_listener; EventEmitterListenerID listener_id;
EventEmitterListenerID pre_notify_listener_id;
}; };
std::vector<ListenerEntry> listeners_; std::vector<ListenerEntry> listeners_;
@@ -73,22 +87,24 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override { void play(Ts... x) override {
// If the listener is already set, do nothing // If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_)
return; return;
// Set initial value // Set initial value
this->parent_->set_value(this->buffer_.value(x...)); this->parent_->set_value(this->buffer_.value(x...));
// Set the listener for read events // Set the listener for read events
this->parent_->on_read([this, x...](uint16_t id) { this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on(
// Set the value of the characteristic every time it is read BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) {
this->parent_->set_value(this->buffer_.value(x...)); // Set the value of the characteristic every time it is read
}); this->parent_->set_value(this->buffer_.value(x...));
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener( BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
} }
protected: protected:
BLECharacteristic *parent_; BLECharacteristic *parent_;
EventEmitterListenerID listener_id_;
}; };
#endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION

View File

@@ -1,13 +1,14 @@
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,
@@ -23,7 +24,6 @@ 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,12 +38,19 @@ 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__)
@@ -121,15 +128,6 @@ 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")
@@ -152,12 +150,24 @@ 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): cv.All( cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_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(
@@ -214,11 +224,48 @@ 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,
) )
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant def validate_remaining_connections(config):
data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {})
slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, [])
used_slots = len(slots)
if used_slots <= config[CONF_MAX_CONNECTIONS]:
return config
slot_users = ", ".join(slots)
if used_slots < IDF_MAX_CONNECTIONS:
_LOGGER.warning(
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
"connection slot(s) out of available configured maximum %d connection "
"slot(s); The system automatically increased `%s` to %d to match the "
"number of used connection slot(s) by components: %s.",
CONF_MAX_CONNECTIONS,
used_slots,
config[CONF_MAX_CONNECTIONS],
CONF_MAX_CONNECTIONS,
used_slots,
slot_users,
)
config[CONF_MAX_CONNECTIONS] = used_slots
return config
msg = (
f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: "
f"components attempted to consume {used_slots} connection slot(s) "
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
)
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
raise cv.Invalid(msg)
FINAL_VALIDATE_SCHEMA = cv.All(
validate_remaining_connections, esp32_ble.validate_variant
)
ESP_BLE_DEVICE_SCHEMA = cv.Schema( ESP_BLE_DEVICE_SCHEMA = cv.Schema(
{ {
@@ -298,8 +345,10 @@ 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)
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
# configured in esp32_ble component based on max_connections setting add_idf_sdkconfig_option(
"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,16 +67,8 @@ 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();
} }
@@ -94,14 +86,14 @@ bool ESP32Can::setup_internal() {
} }
// Install TWAI driver // Install TWAI driver
if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) { if (twai_driver_install(&g_config, &t_config, &f_config) != 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_v2(this->twai_handle_) != ESP_OK) { if (twai_start() != ESP_OK) {
// Failed to start driver // Failed to start driver
this->mark_failed(); this->mark_failed();
return false; return false;
@@ -110,11 +102,6 @@ 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;
} }
@@ -137,7 +124,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_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { if (twai_transmit(&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;
@@ -145,14 +132,9 @@ 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_v2(this->twai_handle_, &message, 0) != ESP_OK) { if (twai_receive(&message, 0) != ESP_OK) {
return canbus::ERROR_NOMSG; return canbus::ERROR_NOMSG;
} }

View File

@@ -5,8 +5,6 @@
#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 {
@@ -31,7 +29,6 @@ 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

@@ -38,7 +38,8 @@ void ESP32ImprovComponent::setup() {
}); });
} }
#endif #endif
global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
// Start with loop disabled - will be enabled by start() when needed // Start with loop disabled - will be enabled by start() when needed
this->disable_loop(); this->disable_loop();
@@ -56,11 +57,12 @@ void ESP32ImprovComponent::setup_characteristics() {
this->error_->add_descriptor(error_descriptor); this->error_->add_descriptor(error_descriptor);
this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
this->rpc_->on_write([this](std::span<const uint8_t> data, uint16_t id) { this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
if (!data.empty()) { BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); if (!data.empty()) {
} this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
}); }
});
BLEDescriptor *rpc_descriptor = new BLE2902(); BLEDescriptor *rpc_descriptor = new BLE2902();
this->rpc_->add_descriptor(rpc_descriptor); this->rpc_->add_descriptor(rpc_descriptor);

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 (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { for (int32_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,67 +614,24 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
return false; return false;
} }
// Generate nonce - hasher must be created and used in same stack frame // Generate nonce with appropriate hasher
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: bool success = false;
// 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) {
hasher = &sha_hasher; sha256::SHA256 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) {
hasher = &md5_hasher; md5::MD5Digest md5_hasher;
success = this->prepare_auth_nonce_(&md5_hasher);
} }
#endif #endif
const size_t hex_size = hasher->get_size() * 2; if (!success) {
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
@@ -721,41 +678,89 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
} }
// We have all the data, verify it // We have all the data, verify it
const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1); bool matches = false;
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) {
hasher = &sha_hasher; sha256::SHA256 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) {
hasher = &md5_hasher; md5::MD5Digest 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[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too char log_buf[hex_size + 1];
// 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';
@@ -773,18 +778,7 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
#endif #endif
// Compare response // Compare response
bool matches = hasher->equals_hex(response); return 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,6 +47,8 @@ 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,20 +41,17 @@ 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) { \
this->log_error_and_mark_failed_(err, message); \ ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
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) { \
this->log_error_and_mark_failed_(err, message); \ ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
this->mark_failed(); \
return ret; \ return ret; \
} }

View File

@@ -106,7 +106,6 @@ 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);
@@ -163,7 +162,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 ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) #if defined(USE_ARDUINO) || 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

@@ -0,0 +1,5 @@
CODEOWNERS = ["@Rapsssito"]
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
CONFIG_SCHEMA = {}

View File

@@ -0,0 +1,117 @@
#pragma once
#include <vector>
#include <functional>
#include <limits>
#include "esphome/core/log.h"
namespace esphome {
namespace event_emitter {
using EventEmitterListenerID = uint32_t;
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this)
// and a list of arguments. Supports multiple listeners for each event.
template<typename EvtType, typename... Args> class EventEmitter {
public:
EventEmitterListenerID on(EvtType event, std::function<void(Args...)> listener) {
EventEmitterListenerID listener_id = this->get_next_id_();
// Find or create event entry
EventEntry *entry = this->find_or_create_event_(event);
entry->listeners.push_back({listener_id, listener});
return listener_id;
}
void off(EvtType event, EventEmitterListenerID id) {
EventEntry *entry = this->find_event_(event);
if (entry == nullptr)
return;
// Remove listener with given id
for (auto it = entry->listeners.begin(); it != entry->listeners.end(); ++it) {
if (it->id == id) {
// Swap with last and pop for efficient removal
*it = entry->listeners.back();
entry->listeners.pop_back();
// Remove event entry if no more listeners
if (entry->listeners.empty()) {
this->remove_event_(event);
}
return;
}
}
}
protected:
void emit_(EvtType event, Args... args) {
EventEntry *entry = this->find_event_(event);
if (entry == nullptr)
return;
// Call all listeners for this event
for (const auto &listener : entry->listeners) {
listener.callback(args...);
}
}
private:
struct Listener {
EventEmitterListenerID id;
std::function<void(Args...)> callback;
};
struct EventEntry {
EvtType event;
std::vector<Listener> listeners;
};
EventEmitterListenerID get_next_id_() {
// Simple incrementing ID, wrapping around at max
EventEmitterListenerID next_id = (this->current_id_ + 1);
if (next_id == INVALID_LISTENER_ID) {
next_id = 1;
}
this->current_id_ = next_id;
return this->current_id_;
}
EventEntry *find_event_(EvtType event) {
for (auto &entry : this->events_) {
if (entry.event == event) {
return &entry;
}
}
return nullptr;
}
EventEntry *find_or_create_event_(EvtType event) {
EventEntry *entry = this->find_event_(event);
if (entry != nullptr)
return entry;
// Create new event entry
this->events_.push_back({event, {}});
return &this->events_.back();
}
void remove_event_(EvtType event) {
for (auto it = this->events_.begin(); it != this->events_.end(); ++it) {
if (it->event == event) {
// Swap with last and pop
*it = this->events_.back();
this->events_.pop_back();
return;
}
}
}
std::vector<EventEntry> events_;
EventEmitterListenerID current_id_ = 0;
};
} // namespace event_emitter
} // namespace esphome

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_ != std::numeric_limits<uint32_t>::max()) { if (this->new_password_ != -1) {
if (this->set_password_()) if (this->set_password_())
return; return;
} else { } else {

View File

@@ -6,7 +6,6 @@
#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 {
@@ -178,7 +177,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_ = std::numeric_limits<uint32_t>::max(); uint32_t new_password_ = -1;
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 && static_cast<uint32_t>(y) < y_offset + this->height_) if (y >= y_offset && y < y_offset + this->height_)
buff->draw_pixel_at(x, y, c); buff->draw_pixel_at(x, y, c);
}; };
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {

View File

@@ -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; max_item_index >= 0 && i <= static_cast<size_t>(max_item_index); i++) { for (size_t i = 0; i <= 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,8 +174,7 @@ 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 = static_cast<size_t>(first_item_index); for (size_t i = first_item_index; i <= last_item_index; i++) {
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: %zu (should be >= %zu)", data_size, this->real_control_packet_size_); ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_);
} }
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
@@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
size_t expected_size = size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
if (size < expected_size) { if (size < expected_size) {
ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size); ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
} }
uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];

View File

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

View File

@@ -7,20 +7,24 @@ namespace hdc1080 {
static const char *const TAG = "hdc1080"; static const char *const TAG = "hdc1080";
static const uint8_t HDC1080_ADDRESS = 0x40; // 0b1000000 from datasheet
static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02; static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02;
static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00; static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00;
static const uint8_t HDC1080_CMD_HUMIDITY = 0x01; static const uint8_t HDC1080_CMD_HUMIDITY = 0x01;
void HDC1080Component::setup() { void HDC1080Component::setup() {
const uint8_t config[2] = {0x00, 0x00}; // resolution 14bit for both humidity and temperature const uint8_t data[2] = {
0b00000000, // resolution 14bit for both humidity and temperature
0b00000000 // reserved
};
// if configuration fails - there is a problem if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) {
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { // as instruction is same as powerup defaults (for now), interpret as warning if this fails
this->mark_failed(); ESP_LOGW(TAG, "HDC1080 initial config instruction error");
this->status_set_warning();
return; return;
} }
} }
void HDC1080Component::dump_config() { void HDC1080Component::dump_config() {
ESP_LOGCONFIG(TAG, "HDC1080:"); ESP_LOGCONFIG(TAG, "HDC1080:");
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
@@ -31,51 +35,39 @@ void HDC1080Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Humidity", this->humidity_);
} }
void HDC1080Component::update() { void HDC1080Component::update() {
// regardless of what sensor/s are defined in yaml configuration uint16_t raw_temp;
// the hdc1080 setup configuration used, requires both temperature and humidity to be read
this->status_clear_warning();
if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) { if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
delay(20);
if (this->read(reinterpret_cast<uint8_t *>(&raw_temp), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
raw_temp = i2c::i2ctohs(raw_temp);
float temp = raw_temp * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40
this->temperature_->publish_state(temp);
this->set_timeout(20, [this]() { uint16_t raw_humidity;
uint16_t raw_temperature; if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) {
if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) { this->status_set_warning();
this->status_set_warning(); return;
return; }
} delay(20);
if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
raw_humidity = i2c::i2ctohs(raw_humidity);
float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100
this->humidity_->publish_state(humidity);
if (this->temperature_ != nullptr) { ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity);
raw_temperature = i2c::i2ctohs(raw_temperature); this->status_clear_warning();
float temperature = raw_temperature * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40
this->temperature_->publish_state(temperature);
}
if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
this->set_timeout(20, [this]() {
uint16_t raw_humidity;
if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
if (this->humidity_ != nullptr) {
raw_humidity = i2c::i2ctohs(raw_humidity);
float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100
this->humidity_->publish_state(humidity);
}
});
});
} }
float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; }
} // namespace hdc1080 } // namespace hdc1080
} // namespace esphome } // namespace esphome

View File

@@ -12,11 +12,13 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice {
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
/// Setup the sensor and check for connection.
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
/// Retrieve the latest sensor values. This operation takes approximately 16ms.
void update() override; void update() override;
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override;
protected: protected:
sensor::Sensor *temperature_{nullptr}; sensor::Sensor *temperature_{nullptr};

View File

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

View File

@@ -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 (size_t i = 0; i < len; i += 2) { for (int 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 != static_cast<size_t>(h); y++) { for (size_t y = 0; y != 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 == static_cast<size_t>(w)) { if (++pixel == w) {
pixel = 0; pixel = 0;
ptr += (x_pad + x_offset) * 2; ptr += (x_pad + x_offset) * 2;
} }

View File

@@ -19,19 +19,15 @@ 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(reinterpret_cast<const uint8_t *>(data.c_str()), data.size()); JsonDocument doc = parse_json(data);
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 uint8_t *data, size_t len) { JsonDocument parse_json(const char *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);
@@ -47,7 +43,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
if (err == DeserializationError::Ok) { if (err == DeserializationError::Ok) {
return json_document; return json_document;
} else if (err == DeserializationError::NoMemory) { } else if (err == DeserializationError::NoMemory) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source buffer smaller");
return JsonObject(); // return unbound object return JsonObject(); // return unbound object
} }
ESP_LOGE(TAG, "Parse error: %s", err.c_str()); ESP_LOGE(TAG, "Parse error: %s", err.c_str());
@@ -55,6 +51,8 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
JsonDocument parse_json(const std::string &data) { return parse_json(data.c_str(), data.size()); }
std::string JsonBuilder::serialize() { std::string JsonBuilder::serialize() {
if (doc_.overflowed()) { if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow"); ESP_LOGE(TAG, "JSON document overflow");

View File

@@ -2,7 +2,6 @@
#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
@@ -50,13 +49,10 @@ 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 uint8_t *data, size_t len); JsonDocument parse_json(const std::string &data);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) /// Parse JSON from a buffer and return the root JsonDocument (or an unbound object on error)
inline JsonDocument parse_json(const std::string &data) { JsonDocument parse_json(const char *data, size_t len);
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 (size_t i = 0; i < this->custom_sensors_.size(); i++) { for (int 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 (size_t i = 0; i < this->custom_commands_.size(); i++) { for (int 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(uint32_t min_length) { this->min_length_ = min_length; }; void set_min_length(int min_length) { this->min_length_ = min_length; };
void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; void set_max_length(int 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);
uint32_t min_length_{0}; int min_length_{0};
uint32_t max_length_{0}; int 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,15 +10,11 @@ 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
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f, #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
float max = 1.0f) { static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float 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), val, min, 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));
} }
@@ -31,6 +27,7 @@ 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)
@@ -47,7 +44,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); \ this->set_flag_(flag, true); \
return *this; \ return *this; \
} }
@@ -184,16 +181,6 @@ 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();
@@ -201,108 +188,141 @@ 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->clear_flag_(FLAG_HAS_COLOR_MODE); this->set_flag_(FLAG_HAS_COLOR_MODE, false);
} }
// 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); this->set_flag_(FLAG_HAS_COLOR_MODE, true);
} }
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_();
// Business logic adjustments before validation // Brightness exists check
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, LOG_STR("brightness"));
this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
}
// Transition length possible check
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, LOG_STR("transitions"));
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
// Color brightness exists check
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB brightness"));
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
}
// RGB exists check
if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->has_blue() && this->blue_ > 0.0f)) {
if (!(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB color"));
this->set_flag_(FLAG_HAS_RED, false);
this->set_flag_(FLAG_HAS_GREEN, false);
this->set_flag_(FLAG_HAS_BLUE, false);
}
}
// White value exists check
if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("white value"));
this->set_flag_(FLAG_HAS_WHITE, false);
}
// Color temperature exists check
if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("color temperature"));
this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
}
// Cold/warm white value exists check
if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
this->set_flag_(FLAG_HAS_COLD_WHITE, false);
this->set_flag_(FLAG_HAS_WARM_WHITE, false);
}
}
#define VALIDATE_RANGE_(name_, upper_name, min, max) \
if (this->has_##name_()) { \
auto val = this->name_##_; \
if (val < (min) || val > (max)) { \
log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \
this->name_##_ = clamp(val, (min), (max)); \
} \
}
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
// Range checks
VALIDATE_RANGE(brightness, "Brightness")
VALIDATE_RANGE(color_brightness, "Color brightness")
VALIDATE_RANGE(red, "Red")
VALIDATE_RANGE(green, "Green")
VALIDATE_RANGE(blue, "Blue")
VALIDATE_RANGE(white, "White")
VALIDATE_RANGE(cold_white, "Cold white")
VALIDATE_RANGE(warm_white, "Warm white")
VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
// 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); this->set_flag_(FLAG_HAS_STATE, true);
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()) && !this->has_color_brightness() && if (this->has_red() || this->has_green() || this->has_blue()) {
this->parent_->remote_values.get_color_brightness() == 0.0f) { if (!this->has_color_brightness() && 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); this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
}
} }
// Capability validation // Create color values for the light with this call applied.
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())
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ v.set_brightness(this->brightness_);
if (this->has_##field()) { \ if (this->has_color_brightness())
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ v.set_color_brightness(this->color_brightness_);
v.setter(this->field##_); \ if (this->has_red())
} v.set_red(this->red_);
if (this->has_green())
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") v.set_green(this->green_);
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") if (this->has_blue())
VALIDATE_AND_APPLY(red, set_red, "Red") v.set_blue(this->blue_);
VALIDATE_AND_APPLY(green, set_green, "Green") if (this->has_white())
VALIDATE_AND_APPLY(blue, set_blue, "Blue") v.set_white(this->white_);
VALIDATE_AND_APPLY(white, set_white, "White") if (this->has_color_temperature())
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") v.set_color_temperature(this->color_temperature_);
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") if (this->has_cold_white())
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), v.set_cold_white(this->cold_white_);
traits.get_max_mireds()) if (this->has_warm_white())
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 >0")); log_invalid_parameter(name, LOG_STR("flash length must be greater than zero"));
this->clear_flag_(FLAG_HAS_FLASH); this->set_flag_(FLAG_HAS_FLASH, false);
} }
// validate transition length/flash length/effect not used at the same time // validate transition length/flash length/effect not used at the same time
@@ -310,40 +330,42 @@ 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->clear_flag_(FLAG_HAS_EFFECT); this->set_flag_(FLAG_HAS_EFFECT, false);
} }
// 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->clear_flag_(FLAG_HAS_EFFECT); this->set_flag_(FLAG_HAS_EFFECT, false);
} }
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->clear_flag_(FLAG_HAS_TRANSITION); this->set_flag_(FLAG_HAS_TRANSITION, false);
this->clear_flag_(FLAG_HAS_FLASH); this->set_flag_(FLAG_HAS_FLASH, false);
} }
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->clear_flag_(FLAG_HAS_TRANSITION); this->set_flag_(FLAG_HAS_TRANSITION, false);
} }
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); this->set_flag_(FLAG_HAS_TRANSITION, true);
} }
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->clear_flag_(FLAG_HAS_TRANSITION); this->set_flag_(FLAG_HAS_TRANSITION, false);
} }
if (this->has_transition_() && !supports_transition) if (this->has_transition_() && !supports_transition) {
this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); log_feature_not_supported(name, LOG_STR("transitions"));
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
@@ -352,17 +374,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->clear_flag_(FLAG_HAS_EFFECT); this->set_flag_(FLAG_HAS_EFFECT, false);
} 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); this->set_flag_(FLAG_HAS_EFFECT, true);
} }
} }
// Disable saving for flashes // Disable saving for flashes
if (this->has_flash_()) if (this->has_flash_())
this->clear_flag_(FLAG_SAVE); this->set_flag_(FLAG_SAVE, false);
return v; return v;
} }
@@ -396,12 +418,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); this->set_flag_(FLAG_HAS_COLD_WHITE, true);
this->set_flag_(FLAG_HAS_WARM_WHITE); this->set_flag_(FLAG_HAS_WARM_WHITE, true);
} }
if (this->has_white()) { if (this->has_white()) {
this->brightness_ = this->white_; this->brightness_ = this->white_;
this->set_flag_(FLAG_HAS_BRIGHTNESS); this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
} }
} }
} }
@@ -608,7 +630,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); this->set_flag_(FLAG_HAS_EFFECT, true);
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,10 +4,6 @@
#include <set> #include <set>
namespace esphome { namespace esphome {
// Forward declaration
struct LogString;
namespace light { namespace light {
class LightState; class LightState;
@@ -211,14 +207,14 @@ class LightCall {
FLAG_SAVE = 1 << 15, FLAG_SAVE = 1 << 15,
}; };
inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag - defaults to true for common case // Helper to set flag
void set_flag_(FieldFlags flag, bool value = true) { void set_flag_(FieldFlags flag, bool value) {
if (value) { if (value) {
this->flags_ |= flag; this->flags_ |= flag;
} else { } else {
@@ -226,12 +222,6 @@ 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

@@ -1,39 +0,0 @@
#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

@@ -1,19 +0,0 @@
#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

@@ -1,34 +0,0 @@
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

@@ -5,7 +5,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include <initializer_list> #include <set>
namespace esphome { namespace esphome {
namespace lock { namespace lock {
@@ -44,22 +44,16 @@ class LockTraits {
bool get_assumed_state() const { return this->assumed_state_; } bool get_assumed_state() const { return this->assumed_state_; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); } bool supports_state(LockState state) const { return supported_states_.count(state); }
void set_supported_states(std::initializer_list<LockState> states) { std::set<LockState> get_supported_states() const { return supported_states_; }
supported_states_mask_ = 0; void set_supported_states(std::set<LockState> states) { supported_states_ = std::move(states); }
for (auto state : states) { void add_supported_state(LockState state) { supported_states_.insert(state); }
supported_states_mask_ |= (1 << state);
}
}
uint8_t get_supported_states_mask() const { return supported_states_mask_; }
void set_supported_states_mask(uint8_t mask) { supported_states_mask_ = mask; }
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
protected: protected:
bool supports_open_{false}; bool supports_open_{false};
bool requires_code_{false}; bool requires_code_{false};
bool assumed_state_{false}; bool assumed_state_{false};
uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)}; std::set<LockState> supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED};
}; };
/** This class is used to encode all control actions on a lock device. /** This class is used to encode all control actions on a lock device.

View File

@@ -95,7 +95,6 @@ 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 = {
@@ -250,7 +249,6 @@ 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),
@@ -293,12 +291,8 @@ async def to_code(config):
) )
cg.add(log.pre_setup()) cg.add(log.pre_setup())
# Enable runtime tag levels if logs are configured or explicitly enabled for tag, log_level in config[CONF_LOGS].items():
logs_config = config[CONF_LOGS] cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
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)
@@ -449,7 +443,6 @@ 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,11 +148,9 @@ 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_;
} }
@@ -222,9 +220,7 @@ 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; }
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
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_; }
@@ -275,11 +271,9 @@ 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, LOG_STR_ARG(LOG_LEVELS[it.second])); ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), 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,38 +36,29 @@ struct device;
namespace esphome::logger { namespace esphome::logger {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Color and letter constants for log levels
// Comparison function for const char* keys in log_levels_ map static const char *const LOG_LEVEL_COLORS[] = {
struct CStrCompare { "", // NONE
bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; } ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR
}; ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING
#endif ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG
// ANSI color code last digit (30-38 range, store only last digit to save RAM) ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE
'\0', // NONE ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
}; };
static constexpr char LOG_LEVEL_LETTER_CHARS[] = { static const char *const LOG_LEVEL_LETTERS[] = {
'\0', // NONE "", // NONE
'E', // ERROR "E", // ERROR
'W', // WARNING "W", // WARNING
'I', // INFO "I", // INFO
'C', // CONFIG "C", // CONFIG
'D', // DEBUG "D", // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's) "V", // VERBOSE
"VV", // VERY_VERBOSE
}; };
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
#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)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
@@ -140,10 +131,8 @@ 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 char *tag, uint8_t log_level); void set_log_level(const std::string &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 ==========
@@ -226,6 +215,14 @@ class Logger : public Component {
} }
} }
// Format string to explicit buffer with varargs
inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg);
va_end(arg);
}
#ifndef USE_HOST #ifndef USE_HOST
const LogString *get_uart_selection_(); const LogString *get_uart_selection_();
#endif #endif
@@ -251,9 +248,7 @@ class Logger : public Component {
#endif #endif
// Large objects (internally aligned) // Large objects (internally aligned)
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS std::map<std::string, uint8_t> log_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
@@ -323,76 +318,26 @@ class Logger : public Component {
} }
#endif #endif
static inline void copy_string(char *buffer, uint16_t &pos, const char *str) {
const size_t len = strlen(str);
// Intentionally no null terminator, building larger string
memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result)
pos += len;
}
static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) {
if (level == 0)
return;
// Construct ANSI escape sequence: "\033[{bold};3{color}m"
// Example: "\033[1;31m" for ERROR (bold red)
buffer[pos++] = '\033';
buffer[pos++] = '[';
buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold
buffer[pos++] = ';';
buffer[pos++] = '3';
buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level];
buffer[pos++] = 'm';
}
inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name,
char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
uint16_t pos = *buffer_at; // Format header
// Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes // uint8_t level is already bounded 0-255, just ensure it's <= 7
if (pos + MAX_HEADER_SIZE > buffer_size) if (level > 7)
return; level = 7;
// Construct: <color>[LEVEL][tag:line]: const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
write_ansi_color_for_level(buffer, pos, level); const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
buffer[pos++] = '[';
if (level != 0) {
if (level >= 7) {
buffer[pos++] = 'V'; // VERY_VERBOSE = "VV"
buffer[pos++] = 'V';
} else {
buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level];
}
}
buffer[pos++] = ']';
buffer[pos++] = '[';
copy_string(buffer, pos, tag);
buffer[pos++] = ':';
// Format line number without modulo operations (passed by value, safe to mutate)
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
buffer[pos++] = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
buffer[pos++] = '0' + hundreds;
buffer[pos++] = '0' + tens;
buffer[pos++] = '0' + (remainder - tens * 10);
buffer[pos++] = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
if (thread_name != nullptr) { if (thread_name != nullptr) {
write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name // Non-main task with thread name
buffer[pos++] = '['; this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,
copy_string(buffer, pos, thread_name); ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color);
buffer[pos++] = ']'; return;
write_ansi_color_for_level(buffer, pos, level); // Restore original color
} }
#endif #endif
// Main task or non ESP32/LibreTiny platform
buffer[pos++] = ':'; this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line);
buffer[pos++] = ' ';
*buffer_at = pos;
} }
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,

View File

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

View File

@@ -3,18 +3,11 @@
#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,7 +2,6 @@
#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;
@@ -29,30 +28,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 = std::numeric_limits<size_t>::max(); size_t idx = -1;
while (idx == std::numeric_limits<size_t>::max() && i < size) { while (idx == -1 && i < size) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i++; i++;
} }
if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size) if (idx == -1 || i + 1 >= size)
return val; return val;
return array[i + 1]; return array[i + 1];
} }
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1; size_t i = size - 1;
size_t idx = std::numeric_limits<size_t>::max(); size_t idx = -1;
while (idx == std::numeric_limits<size_t>::max() && i > 0) { while (idx == -1 && i > 0) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i--; i--;
} }
if (idx == std::numeric_limits<size_t>::max() || i == 0) if (idx == -1 || i == 0)
return val; return val;
return array[i - 1]; return array[i - 1];
} }

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