Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
b22a8b00bf [api] Guarantee SSO for action call timeout keys 2026-01-12 16:14:13 -10:00
278 changed files with 12797 additions and 16420 deletions

View File

@@ -27,7 +27,6 @@
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x
- [ ] nRF52840
## Example entry for `config.yaml`:

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with:
category: "/language:${{matrix.language}}"

View File

@@ -255,7 +255,6 @@ esphome/components/inkplate/* @jesserockz @JosipKuci
esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/ir_rf_proxy/* @kbx81
esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @esphome/core
esphome/components/kamstrup_kmp/* @cfeenstra1024

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.1.3
PROJECT_NUMBER = 2026.1.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -1,6 +1,5 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from datetime import datetime
import functools
import getpass
@@ -932,21 +931,11 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
return dashboard.start_dashboard(args)
def run_multiple_configs(
files: list, command_builder: Callable[[str], list[str]]
) -> int:
"""Run a command for each configuration file in a subprocess.
Args:
files: List of configuration files to process.
command_builder: Callable that takes a file path and returns a command list.
Returns:
Number of failed files.
"""
def command_update_all(args: ArgsProtocol) -> int | None:
import click
success = {}
files = list_yaml_files(args.configuration)
twidth = 60
def print_bar(middle_text):
@@ -956,19 +945,17 @@ def run_multiple_configs(
safe_print(f"{half_line}{middle_text}{half_line}")
for f in files:
f_path = Path(f) if not isinstance(f, Path) else f
if any(f_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", f_path)
continue
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
safe_print("-" * twidth)
safe_print()
cmd = command_builder(f)
rc = run_external_process(*cmd)
if CORE.dashboard:
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
else:
rc = run_external_process(
"esphome", "run", f, "--no-logs", "--device", "OTA"
)
if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
success[f] = True
@@ -983,8 +970,6 @@ def run_multiple_configs(
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if f not in success:
continue # Skipped file
if success[f]:
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
@@ -993,17 +978,6 @@ def run_multiple_configs(
return failed
def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json
@@ -1554,48 +1528,38 @@ def run_esphome(argv):
_LOGGER.info("ESPHome %s", const.__version__)
# Multiple configurations: use subprocesses to avoid state leakage
# between compilations (e.g., LVGL touchscreen state in module globals)
if len(args.configuration) > 1:
# Build command by reusing argv, replacing all configs with single file
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)
for conf_path in args.configuration:
conf_path = Path(conf_path)
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
continue
return run_multiple_configs(args.configuration, build_command)
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
# Single configuration
conf_path = Path(args.configuration[0])
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
return 0
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
try:
rc = POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
if rc != 0:
return rc
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
return 1
try:
return POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
CORE.reset()
return 0
def main():

View File

@@ -31,8 +31,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states

View File

@@ -4,7 +4,6 @@ import logging
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.logger import request_log_listener
from esphome.config_helpers import get_logger_level
import esphome.config_validation as cv
from esphome.const import (
@@ -327,9 +326,6 @@ async def to_code(config: ConfigType) -> None:
# Track controller registration for StaticVector sizing
CORE.register_controller()
# Request a log listener slot for API log streaming
request_log_listener()
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))

View File

@@ -265,7 +265,8 @@ void APIConnection::loop() {
// If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
ESP_LOGW(TAG, "Buffer full, ping queued");
this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE);
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE,
PingRequest::ESTIMATED_SIZE);
this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
}
}
@@ -304,8 +305,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
#ifdef HAS_PROTO_MESSAGE_DUMP
// If in log-only mode, just log and return
if (conn->flags_.log_only_mode) {
DumpBuffer dump_buf;
conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
conn->log_send_message_(msg.message_name(), msg.dump());
return 1; // Return non-zero to indicate "success" for logging
}
#endif
@@ -361,8 +361,8 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
#ifdef USE_BINARY_SENSOR
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE,
BinarySensorStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
BinarySensorStateResponse::MESSAGE_TYPE, BinarySensorStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -388,7 +388,8 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
#ifdef USE_COVER
bool APIConnection::send_cover_state(cover::Cover *cover) {
return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE,
CoverStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -428,7 +429,8 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#ifdef USE_FAN
bool APIConnection::send_fan_state(fan::Fan *fan) {
return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE,
FanStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -479,7 +481,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
#ifdef USE_LIGHT
bool APIConnection::send_light_state(light::LightState *light) {
return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE,
LightStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -565,7 +568,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
#ifdef USE_SENSOR
bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE,
SensorStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -593,7 +597,8 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
#ifdef USE_SWITCH
bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE,
SwitchStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -627,8 +632,8 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) {
#ifdef USE_TEXT_SENSOR
bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
return this->send_message_smart_(text_sensor, TextSensorStateResponse::MESSAGE_TYPE,
TextSensorStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state,
TextSensorStateResponse::MESSAGE_TYPE, TextSensorStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -652,7 +657,8 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
#ifdef USE_CLIMATE
bool APIConnection::send_climate_state(climate::Climate *climate) {
return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE,
ClimateStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -747,7 +753,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
#ifdef USE_NUMBER
bool APIConnection::send_number_state(number::Number *number) {
return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE,
NumberStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -781,7 +788,8 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
#ifdef USE_DATETIME_DATE
bool APIConnection::send_date_state(datetime::DateEntity *date) {
return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE,
DateStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -809,7 +817,8 @@ void APIConnection::date_command(const DateCommandRequest &msg) {
#ifdef USE_DATETIME_TIME
bool APIConnection::send_time_state(datetime::TimeEntity *time) {
return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE,
TimeStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -837,8 +846,8 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
#ifdef USE_DATETIME_DATETIME
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE,
DateTimeStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state,
DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -868,7 +877,8 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
#ifdef USE_TEXT
bool APIConnection::send_text_state(text::Text *text) {
return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE,
TextStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -900,7 +910,8 @@ void APIConnection::text_command(const TextCommandRequest &msg) {
#ifdef USE_SELECT
bool APIConnection::send_select_state(select::Select *select) {
return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE,
SelectStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -944,7 +955,8 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg
#ifdef USE_LOCK
bool APIConnection::send_lock_state(lock::Lock *a_lock) {
return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE,
LockStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -984,7 +996,8 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
#ifdef USE_VALVE
bool APIConnection::send_valve_state(valve::Valve *valve) {
return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE,
ValveStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -1018,8 +1031,8 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) {
#ifdef USE_MEDIA_PLAYER
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE,
MediaPlayerStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state,
MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -1301,7 +1314,8 @@ void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) {
#ifdef USE_ALARM_CONTROL_PANEL
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
AlarmControlPanelStateResponse::MESSAGE_TYPE,
AlarmControlPanelStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
@@ -1354,8 +1368,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#ifdef USE_WATER_HEATER
bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE,
WaterHeaterStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state,
WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -1404,11 +1418,10 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
void APIConnection::send_event(event::Event *event) {
this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE,
event->get_last_event_type_index());
void APIConnection::send_event(event::Event *event, StringRef event_type) {
// get_last_event_type() returns StringRef pointing to null-terminated string literals from codegen
this->send_message_smart_(event, MessageCreator(event_type.c_str()), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) {
@@ -1459,7 +1472,8 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
#ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) {
return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE);
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE,
UpdateStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -1712,16 +1726,17 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
// HA state max length is 255, so 256 byte buffer covers all cases
char state_buf[256];
size_t copy_len = msg.state.size();
if (copy_len >= sizeof(state_buf)) {
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
}
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
if (copy_len > 0) {
memcpy(state_buf, msg.state.c_str(), copy_len);
}
state_buf[copy_len] = '\0';
it.callback(StringRef(state_buf, copy_len));
}
}
#endif
@@ -1844,8 +1859,23 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false;
}
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
this->helper_->set_nodelay_for_message(is_log_message);
// Toggle Nagle's algorithm based on message type to prevent log messages from
// filling the TCP send buffer and crowding out important state updates.
//
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
// message with `option (no_delay) = false;` in api.proto, indicating it should
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
//
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
// (~200ms), buffer filling, or when a state update triggers a flush.
//
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
// for immediate delivery. These are time-sensitive and should not be delayed.
//
// This must be done proactively BEFORE the buffer fills up - checking buffer
// state here would be too late since we'd already be in a degraded state.
this->helper_->set_nodelay(!is_log_message);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
if (err == APIError::WOULD_BLOCK)
@@ -1866,31 +1896,30 @@ void APIConnection::on_fatal_error() {
this->flags_.remove = true;
}
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
uint8_t aux_data_index) {
void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type,
uint8_t estimated_size) {
// Check if we already have a message of this type for this entity
// This provides deduplication per entity/message_type combination
// O(n) but optimized for RAM and not performance.
// Skip deduplication for events - they are edge-triggered, every occurrence matters
#ifdef USE_EVENT
if (message_type != EventResponse::MESSAGE_TYPE)
#endif
{
for (const auto &item : items) {
if (item.entity == entity && item.message_type == message_type)
return; // Already queued
for (auto &item : items) {
if (item.entity == entity && item.message_type == message_type) {
// Replace with new creator
item.creator = creator;
return;
}
}
// No existing item found (or event), add new one
items.push_back({entity, message_type, estimated_size, aux_data_index});
// No existing item found, add new one
items.emplace_back(entity, creator, message_type, estimated_size);
}
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
uint8_t estimated_size) {
// Add high priority message and swap to front
// This avoids expensive vector::insert which shifts all elements
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
items.emplace_back(entity, creator, message_type, estimated_size);
if (items.size() > 1) {
// Swap the new high-priority item to the front
std::swap(items.front(), items.back());
@@ -1929,17 +1958,19 @@ void APIConnection::process_batch_() {
if (num_items == 1) {
const auto &item = this->deferred_batch_[0];
// Let dispatch_message_ calculate size and encode if it fits
uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits<uint16_t>::max(), true);
// Let the creator calculate size and encode if it fits
uint16_t payload_size =
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log message after send attempt for VV debugging
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
this->log_batch_item_(item);
#endif
this->clear_batch_();
} else if (payload_size == 0) {
// Message too large to fit in available space
// Message too large
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
this->clear_batch_();
}
@@ -1984,9 +2015,9 @@ void APIConnection::process_batch_() {
// Process items and encode directly to buffer (up to our limit)
for (size_t i = 0; i < messages_to_process; i++) {
const auto &item = this->deferred_batch_[i];
// Try to encode message via dispatch
// The dispatch function calculates overhead to determine if the message fits
uint16_t payload_size = this->dispatch_message_(item, remaining_size, false);
// Try to encode message
// The creator will calculate overhead to determine if the message fits
uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type);
if (payload_size == 0) {
// Message won't fit, stop processing
@@ -2052,129 +2083,18 @@ void APIConnection::process_batch_() {
}
}
// Dispatch message encoding based on message_type
// Switch assigns function pointer, single call site for smaller code size
uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
bool is_single) {
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single, uint8_t message_type) const {
#ifdef USE_EVENT
// Events need aux_data_index to look up event type from entity
if (item.message_type == EventResponse::MESSAGE_TYPE) {
// Skip if aux_data_index is invalid (should never happen in normal operation)
if (item.aux_data_index == DeferredBatch::AUX_DATA_UNUSED)
return 0;
auto *event = static_cast<event::Event *>(item.entity);
return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)),
this, remaining_size, is_single);
// Special case: EventResponse uses const char * pointer
if (message_type == EventResponse::MESSAGE_TYPE) {
auto *e = static_cast<event::Event *>(entity);
return APIConnection::try_send_event_response(e, StringRef(data_.const_char_ptr), conn, remaining_size, is_single);
}
#endif
// All other message types use function pointer lookup via switch
MessageCreatorPtr func = nullptr;
// Macros to reduce repetitive switch cases
#define CASE_STATE_INFO(entity_name, StateResp, InfoResp) \
case StateResp::MESSAGE_TYPE: \
func = &try_send_##entity_name##_state; \
break; \
case InfoResp::MESSAGE_TYPE: \
func = &try_send_##entity_name##_info; \
break;
#define CASE_INFO_ONLY(entity_name, InfoResp) \
case InfoResp::MESSAGE_TYPE: \
func = &try_send_##entity_name##_info; \
break;
switch (item.message_type) {
#ifdef USE_BINARY_SENSOR
CASE_STATE_INFO(binary_sensor, BinarySensorStateResponse, ListEntitiesBinarySensorResponse)
#endif
#ifdef USE_COVER
CASE_STATE_INFO(cover, CoverStateResponse, ListEntitiesCoverResponse)
#endif
#ifdef USE_FAN
CASE_STATE_INFO(fan, FanStateResponse, ListEntitiesFanResponse)
#endif
#ifdef USE_LIGHT
CASE_STATE_INFO(light, LightStateResponse, ListEntitiesLightResponse)
#endif
#ifdef USE_SENSOR
CASE_STATE_INFO(sensor, SensorStateResponse, ListEntitiesSensorResponse)
#endif
#ifdef USE_SWITCH
CASE_STATE_INFO(switch, SwitchStateResponse, ListEntitiesSwitchResponse)
#endif
#ifdef USE_BUTTON
CASE_INFO_ONLY(button, ListEntitiesButtonResponse)
#endif
#ifdef USE_TEXT_SENSOR
CASE_STATE_INFO(text_sensor, TextSensorStateResponse, ListEntitiesTextSensorResponse)
#endif
#ifdef USE_CLIMATE
CASE_STATE_INFO(climate, ClimateStateResponse, ListEntitiesClimateResponse)
#endif
#ifdef USE_NUMBER
CASE_STATE_INFO(number, NumberStateResponse, ListEntitiesNumberResponse)
#endif
#ifdef USE_DATETIME_DATE
CASE_STATE_INFO(date, DateStateResponse, ListEntitiesDateResponse)
#endif
#ifdef USE_DATETIME_TIME
CASE_STATE_INFO(time, TimeStateResponse, ListEntitiesTimeResponse)
#endif
#ifdef USE_DATETIME_DATETIME
CASE_STATE_INFO(datetime, DateTimeStateResponse, ListEntitiesDateTimeResponse)
#endif
#ifdef USE_TEXT
CASE_STATE_INFO(text, TextStateResponse, ListEntitiesTextResponse)
#endif
#ifdef USE_SELECT
CASE_STATE_INFO(select, SelectStateResponse, ListEntitiesSelectResponse)
#endif
#ifdef USE_LOCK
CASE_STATE_INFO(lock, LockStateResponse, ListEntitiesLockResponse)
#endif
#ifdef USE_VALVE
CASE_STATE_INFO(valve, ValveStateResponse, ListEntitiesValveResponse)
#endif
#ifdef USE_MEDIA_PLAYER
CASE_STATE_INFO(media_player, MediaPlayerStateResponse, ListEntitiesMediaPlayerResponse)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
CASE_STATE_INFO(alarm_control_panel, AlarmControlPanelStateResponse, ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_WATER_HEATER
CASE_STATE_INFO(water_heater, WaterHeaterStateResponse, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_CAMERA
CASE_INFO_ONLY(camera, ListEntitiesCameraResponse)
#endif
#ifdef USE_INFRARED
CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_EVENT
CASE_INFO_ONLY(event, ListEntitiesEventResponse)
#endif
#ifdef USE_UPDATE
CASE_STATE_INFO(update, UpdateStateResponse, ListEntitiesUpdateResponse)
#endif
// Special messages (not entity state/info)
case ListEntitiesDoneResponse::MESSAGE_TYPE:
func = &try_send_list_info_done;
break;
case DisconnectRequest::MESSAGE_TYPE:
func = &try_send_disconnect_request;
break;
case PingRequest::MESSAGE_TYPE:
func = &try_send_ping_request;
break;
default:
return 0;
}
#undef CASE_STATE_INFO
#undef CASE_INFO_ONLY
return func(item.entity, this, remaining_size, is_single);
// All other message types use function pointers
return data_.function_ptr(entity, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,

View File

@@ -12,7 +12,6 @@
#include "esphome/core/string_ref.h"
#include <functional>
#include <limits>
#include <vector>
namespace esphome::api {
@@ -39,8 +38,8 @@ class APIConnection final : public APIServerConnection {
void loop();
bool send_list_info_done() {
return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE,
ListEntitiesDoneResponse::ESTIMATED_SIZE);
return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done,
ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE);
}
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
@@ -179,7 +178,7 @@ class APIConnection final : public APIServerConnection {
#endif
#ifdef USE_EVENT
void send_event(event::Event *event);
void send_event(event::Event *event, StringRef event_type);
#endif
#ifdef USE_UPDATE
@@ -541,17 +540,33 @@ class APIConnection final : public APIServerConnection {
// Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
class MessageCreator {
public:
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
// Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const;
private:
union Data {
MessageCreatorPtr function_ptr;
const char *const_char_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit
};
// Generic batching mechanism for both state updates and entity info
struct DeferredBatch {
// Sentinel value for unused aux_data_index
static constexpr uint8_t AUX_DATA_UNUSED = std::numeric_limits<uint8_t>::max();
struct BatchItem {
EntityBase *entity; // 4 bytes - Entity pointer
uint8_t message_type; // 1 byte - Message type for protocol and dispatch
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
// 1 byte padding
EntityBase *entity; // Entity pointer
MessageCreator creator; // Function that creates the message when needed
uint8_t message_type; // Message type for overhead calculation (max 255)
uint8_t estimated_size; // Estimated message size (max 255 bytes)
// Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {}
};
std::vector<BatchItem> items;
@@ -560,11 +575,10 @@ class APIConnection final : public APIServerConnection {
// No pre-allocation - log connections never use batching, and for
// connections that do, buffers are released after initial sync anyway
// Add item to the batch (with deduplication)
void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = AUX_DATA_UNUSED);
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Clear all items
void clear() {
@@ -578,7 +592,6 @@ class APIConnection final : public APIServerConnection {
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
@@ -650,16 +663,18 @@ class APIConnection final : public APIServerConnection {
this->flags_.batch_scheduled = false;
}
// Dispatch message encoding based on message_type - replaces function pointer storage
// Switch assigns pointer, single call site for smaller code size
uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool is_single);
#ifdef HAS_PROTO_MESSAGE_DUMP
void log_batch_item_(const DeferredBatch::BatchItem &item) {
// Helper to log a proto message from a MessageCreator object
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) {
this->flags_.log_only_mode = true;
this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true);
creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type);
this->flags_.log_only_mode = false;
}
void log_batch_item_(const DeferredBatch::BatchItem &item) {
// Use the helper to log the message
this->log_proto_message_(item.entity, item.creator, item.message_type);
}
#endif
// Helper to check if a message type should bypass batching
@@ -683,31 +698,63 @@ class APIConnection final : public APIServerConnection {
// Helper method to send a message either immediately or via batching
// Tries immediate send if should_send_immediately_() returns true and buffer has space
// Falls back to batching if immediate send fails or isn't applicable
bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index};
if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) &&
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_batch_item_(item);
// Log the message in verbose mode
this->log_proto_message_(entity, MessageCreator(creator), message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
return this->schedule_message_(entity, message_type, estimated_size, aux_data_index);
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Overload for MessageCreator (used by events which need to capture event_type)
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
// Try to send immediately if message type should bypass batching and buffer has space
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, creator, message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
this->deferred_batch_.add_item(entity, message_type, estimated_size, aux_data_index);
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size);
return this->schedule_batch_();
}
// Overload for function pointers (for info messages and current state reads)
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
uint8_t estimated_size) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size);
}
// Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
uint8_t estimated_size) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
return this->schedule_batch_();
}

View File

@@ -120,39 +120,26 @@ class APIFrameHelper {
}
return APIError::OK;
}
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
//
// For non-log messages (sensor data, state updates): Always disable Nagle
// (NODELAY on) for immediate delivery - these are time-sensitive.
//
// For log messages: Use Nagle to coalesce multiple small log packets into
// fewer larger packets, reducing WiFi overhead. However, we limit batching
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
// shared pbufs, but holding data too long waiting for Nagle's timer causes
// buffer exhaustion and dropped messages.
//
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
if (this->nodelay_state_ != NODELAY_ON) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
}
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
if (this->nodelay_state_ == NODELAY_ON) {
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
} else {
this->nodelay_state_++;
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
///
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
/// state updates low-latency (NODELAY enabled). Without this, many small log
/// packets fill the TCP send buffer, crowding out important state updates.
///
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
///
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
/// @return true if successful or already in desired state
bool set_nodelay(bool enable) {
if (this->nodelay_enabled_ == enable)
return true;
int val = enable ? 1 : 0;
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
if (err == 0) {
this->nodelay_enabled_ = enable;
}
return err == 0;
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single operation
@@ -242,18 +229,10 @@ class APIFrameHelper {
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
static constexpr int8_t NODELAY_ON = -1;
static constexpr int8_t LOG_NAGLE_COUNT = 2;
int8_t nodelay_state_{NODELAY_ON};
// Internal helper to set TCP_NODELAY socket option
void set_nodelay_raw_(bool enable) {
int val = enable ? 1 : 0;
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
}
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
// messages to coalesce while keeping state updates low-latency.
bool nodelay_enabled_{true};
// Common initialization for both plaintext and noise protocols
APIError init_common_();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,8 @@ namespace esphome::api {
static const char *const TAG = "api.service";
#ifdef HAS_PROTO_MESSAGE_DUMP
void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) {
ESP_LOGVV(TAG, "send_message %s: %s", name, dump);
}
void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) {
DumpBuffer dump_buf;
ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));
void APIServerConnectionBase::log_send_message_(const char *name, const std::string &dump) {
ESP_LOGVV(TAG, "send_message %s: %s", name, dump.c_str());
}
#endif
@@ -23,7 +19,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
HelloRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_hello_request"), msg);
ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str());
#endif
this->on_hello_request(msg);
break;
@@ -32,7 +28,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
DisconnectRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
#endif
this->on_disconnect_request(msg);
break;
@@ -41,7 +37,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
DisconnectResponse msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_disconnect_response"), msg);
ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
#endif
this->on_disconnect_response(msg);
break;
@@ -50,7 +46,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
PingRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_ping_request"), msg);
ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
#endif
this->on_ping_request(msg);
break;
@@ -59,7 +55,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
PingResponse msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_ping_response"), msg);
ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
#endif
this->on_ping_response(msg);
break;
@@ -68,7 +64,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
DeviceInfoRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_device_info_request"), msg);
ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
#endif
this->on_device_info_request(msg);
break;
@@ -77,7 +73,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ListEntitiesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_list_entities_request"), msg);
ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
#endif
this->on_list_entities_request(msg);
break;
@@ -86,7 +82,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeStatesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_states_request(msg);
break;
@@ -95,7 +91,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeLogsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_logs_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_logs_request(msg);
break;
@@ -105,7 +101,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
CoverCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_cover_command_request"), msg);
ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str());
#endif
this->on_cover_command_request(msg);
break;
@@ -116,7 +112,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
FanCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_fan_command_request"), msg);
ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str());
#endif
this->on_fan_command_request(msg);
break;
@@ -127,7 +123,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
LightCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_light_command_request"), msg);
ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str());
#endif
this->on_light_command_request(msg);
break;
@@ -138,7 +134,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SwitchCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_switch_command_request"), msg);
ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str());
#endif
this->on_switch_command_request(msg);
break;
@@ -149,7 +145,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeHomeassistantServicesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_homeassistant_services_request(msg);
break;
@@ -159,7 +155,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
GetTimeResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_get_time_response"), msg);
ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str());
#endif
this->on_get_time_response(msg);
break;
@@ -169,7 +165,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeHomeAssistantStatesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_home_assistant_states_request(msg);
break;
@@ -180,7 +176,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
HomeAssistantStateResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_home_assistant_state_response"), msg);
ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str());
#endif
this->on_home_assistant_state_response(msg);
break;
@@ -191,7 +187,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_execute_service_request"), msg);
ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str());
#endif
this->on_execute_service_request(msg);
break;
@@ -202,7 +198,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
CameraImageRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_camera_image_request"), msg);
ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str());
#endif
this->on_camera_image_request(msg);
break;
@@ -213,7 +209,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ClimateCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_climate_command_request"), msg);
ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str());
#endif
this->on_climate_command_request(msg);
break;
@@ -224,7 +220,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
NumberCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_number_command_request"), msg);
ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str());
#endif
this->on_number_command_request(msg);
break;
@@ -235,7 +231,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SelectCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_select_command_request"), msg);
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
#endif
this->on_select_command_request(msg);
break;
@@ -246,7 +242,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SirenCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_siren_command_request"), msg);
ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str());
#endif
this->on_siren_command_request(msg);
break;
@@ -257,7 +253,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
LockCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_lock_command_request"), msg);
ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str());
#endif
this->on_lock_command_request(msg);
break;
@@ -268,7 +264,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ButtonCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_button_command_request"), msg);
ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
#endif
this->on_button_command_request(msg);
break;
@@ -279,7 +275,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
MediaPlayerCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_media_player_command_request"), msg);
ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str());
#endif
this->on_media_player_command_request(msg);
break;
@@ -290,7 +286,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeBluetoothLEAdvertisementsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_le_advertisements_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_bluetooth_le_advertisements_request(msg);
break;
@@ -301,7 +297,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothDeviceRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_device_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_device_request(msg);
break;
@@ -312,7 +308,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothGATTGetServicesRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_get_services_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_gatt_get_services_request(msg);
break;
@@ -323,7 +319,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothGATTReadRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_read_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_gatt_read_request(msg);
break;
@@ -334,7 +330,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothGATTWriteRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_write_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_gatt_write_request(msg);
break;
@@ -345,7 +341,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothGATTReadDescriptorRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_read_descriptor_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_gatt_read_descriptor_request(msg);
break;
@@ -356,7 +352,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothGATTWriteDescriptorRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_write_descriptor_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_gatt_write_descriptor_request(msg);
break;
@@ -367,7 +363,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothGATTNotifyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_notify_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_gatt_notify_request(msg);
break;
@@ -378,7 +374,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeBluetoothConnectionsFreeRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_bluetooth_connections_free_request(msg);
break;
@@ -389,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
UnsubscribeBluetoothLEAdvertisementsRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg);
ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
#endif
this->on_unsubscribe_bluetooth_le_advertisements_request(msg);
break;
@@ -400,7 +396,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
SubscribeVoiceAssistantRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_voice_assistant_request"), msg);
ESP_LOGVV(TAG, "on_subscribe_voice_assistant_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_voice_assistant_request(msg);
break;
@@ -411,7 +407,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_response"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_response: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_response(msg);
break;
@@ -422,7 +418,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantEventResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_event_response"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_event_response(msg);
break;
@@ -433,7 +429,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
AlarmControlPanelCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_alarm_control_panel_command_request"), msg);
ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str());
#endif
this->on_alarm_control_panel_command_request(msg);
break;
@@ -444,7 +440,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
TextCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_text_command_request"), msg);
ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str());
#endif
this->on_text_command_request(msg);
break;
@@ -455,7 +451,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
DateCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_date_command_request"), msg);
ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str());
#endif
this->on_date_command_request(msg);
break;
@@ -466,7 +462,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
TimeCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_time_command_request"), msg);
ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str());
#endif
this->on_time_command_request(msg);
break;
@@ -477,7 +473,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantAudio msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_audio"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_audio(msg);
break;
@@ -488,7 +484,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ValveCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_valve_command_request"), msg);
ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str());
#endif
this->on_valve_command_request(msg);
break;
@@ -499,7 +495,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
DateTimeCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_date_time_command_request"), msg);
ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str());
#endif
this->on_date_time_command_request(msg);
break;
@@ -510,7 +506,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantTimerEventResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_timer_event_response"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_timer_event_response(msg);
break;
@@ -521,7 +517,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
UpdateCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_update_command_request"), msg);
ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str());
#endif
this->on_update_command_request(msg);
break;
@@ -532,7 +528,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantAnnounceRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_announce_request"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_announce_request(msg);
break;
@@ -543,7 +539,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantConfigurationRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_configuration_request"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_configuration_request(msg);
break;
@@ -554,7 +550,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
VoiceAssistantSetConfiguration msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_voice_assistant_set_configuration"), msg);
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_set_configuration(msg);
break;
@@ -565,7 +561,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
NoiseEncryptionSetKeyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_noise_encryption_set_key_request"), msg);
ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str());
#endif
this->on_noise_encryption_set_key_request(msg);
break;
@@ -576,7 +572,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
BluetoothScannerSetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_scanner_set_mode_request"), msg);
ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_scanner_set_mode_request(msg);
break;
@@ -587,7 +583,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ZWaveProxyFrame msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_z_wave_proxy_frame"), msg);
ESP_LOGVV(TAG, "on_z_wave_proxy_frame: %s", msg.dump().c_str());
#endif
this->on_z_wave_proxy_frame(msg);
break;
@@ -598,7 +594,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ZWaveProxyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_z_wave_proxy_request"), msg);
ESP_LOGVV(TAG, "on_z_wave_proxy_request: %s", msg.dump().c_str());
#endif
this->on_z_wave_proxy_request(msg);
break;
@@ -609,7 +605,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
HomeassistantActionResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_homeassistant_action_response"), msg);
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
#endif
this->on_homeassistant_action_response(msg);
break;
@@ -620,7 +616,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_water_heater_command_request"), msg);
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
@@ -631,7 +627,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_infrared_rf_transmit_raw_timings_request"), msg);
ESP_LOGVV(TAG, "on_infrared_rf_transmit_raw_timings_request: %s", msg.dump().c_str());
#endif
this->on_infrared_rf_transmit_raw_timings_request(msg);
break;

View File

@@ -12,16 +12,14 @@ class APIServerConnectionBase : public ProtoService {
public:
#ifdef HAS_PROTO_MESSAGE_DUMP
protected:
void log_send_message_(const char *name, const char *dump);
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
void log_send_message_(const char *name, const std::string &dump);
public:
#endif
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
#ifdef HAS_PROTO_MESSAGE_DUMP
DumpBuffer dump_buf;
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, message_type);
}

View File

@@ -241,10 +241,8 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR
@@ -320,13 +318,13 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
for (auto &c : this->clients_)
c->send_event(obj, obj->get_last_event_type());
}
#endif
@@ -335,10 +333,8 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
for (auto &c : this->clients_)
c->send_update_state(obj);
}
#endif
@@ -558,10 +554,8 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
if (!client->flags_.remove && client->is_authenticated()) {
if (!client->flags_.remove && client->is_authenticated())
client->send_time_request();
return; // Only request from one client to avoid clock conflicts
}
}
}
#endif
@@ -621,7 +615,8 @@ void APIServer::on_shutdown() {
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE,
DisconnectRequest::ESTIMATED_SIZE);
}
}
}
@@ -644,6 +639,14 @@ bool APIServer::teardown() {
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
#endif
// SSO-friendly action call key - hex format guarantees max 11 chars ("ac_ffffffff")
// which fits in any std::string SSO buffer (typically 12-15 bytes)
static inline std::string make_action_call_key(uint32_t id) {
char buf[12];
size_t len = snprintf(buf, sizeof(buf), "ac_%x", id);
return std::string(buf, len);
}
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
uint32_t action_call_id = this->next_action_call_id_++;
// Handle wraparound (skip 0 as it means "no call")
@@ -653,8 +656,7 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
// Schedule automatic cleanup after timeout (client will have given up by then)
// Uses numeric ID overload to avoid heap allocation from str_sprintf
this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
this->set_timeout(make_action_call_key(action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
@@ -663,8 +665,8 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn
}
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
// Cancel the timeout for this action call (uses numeric ID overload)
this->cancel_timeout(action_call_id);
// Cancel the timeout for this action call
this->cancel_timeout(make_action_call_key(action_call_id));
// Swap-and-pop is more efficient than remove_if for unordered vectors
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
@@ -680,8 +682,8 @@ void APIServer::unregister_active_action_calls_for_connection(APIConnection *con
// Remove all active action calls for disconnected connection using swap-and-pop
for (size_t i = 0; i < this->active_action_calls_.size();) {
if (this->active_action_calls_[i].connection == conn) {
// Cancel the timeout for this action call (uses numeric ID overload)
this->cancel_timeout(this->active_action_calls_[i].action_call_id);
// Cancel the timeout for this action call
this->cancel_timeout(make_action_call_key(this->active_action_calls_[i].action_call_id));
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();

View File

@@ -9,10 +9,11 @@ namespace esphome::api {
class APIConnection;
// Macro for generating ListEntitiesIterator handlers
// Calls schedule_message_ which dispatches to try_send_*_info
// Calls schedule_message_ with try_send_*_info
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator {

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
if (ptr + field_length > end) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (end - ptr < 4) {
if (ptr + 4 > end) {
return count;
}
ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
if (ptr + field_length > end) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (end - ptr < 4) {
if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
@@ -139,4 +139,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
std::string ProtoMessage::dump() const {
std::string out;
this->dump_to(out);
return out;
}
#endif
} // namespace esphome::api

View File

@@ -362,63 +362,6 @@ class ProtoWriteBuffer {
std::vector<uint8_t> *buffer_;
};
#ifdef HAS_PROTO_MESSAGE_DUMP
/**
* Fixed-size buffer for message dumps - avoids heap allocation.
* Sized to match the logger's default tx_buffer_size (512 bytes)
* since anything larger gets truncated anyway.
*/
class DumpBuffer {
public:
// Matches default tx_buffer_size in logger component
static constexpr size_t CAPACITY = 512;
DumpBuffer() : pos_(0) { buf_[0] = '\0'; }
DumpBuffer &append(const char *str) {
if (str) {
append_impl_(str, strlen(str));
}
return *this;
}
DumpBuffer &append(const char *str, size_t len) {
append_impl_(str, len);
return *this;
}
DumpBuffer &append(size_t n, char c) {
size_t space = CAPACITY - 1 - pos_;
if (n > space)
n = space;
if (n > 0) {
memset(buf_ + pos_, c, n);
pos_ += n;
buf_[pos_] = '\0';
}
return *this;
}
const char *c_str() const { return buf_; }
size_t size() const { return pos_; }
private:
void append_impl_(const char *str, size_t len) {
size_t space = CAPACITY - 1 - pos_;
if (len > space)
len = space;
if (len > 0) {
memcpy(buf_ + pos_, str, len);
pos_ += len;
buf_[pos_] = '\0';
}
}
char buf_[CAPACITY];
size_t pos_;
};
#endif
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
@@ -427,7 +370,8 @@ class ProtoMessage {
// Default implementation for messages with no fields
virtual void calculate_size(ProtoSize &size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
virtual const char *dump_to(DumpBuffer &out) const = 0;
std::string dump() const;
virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
};

View File

@@ -13,11 +13,14 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
CODEOWNERS = ["@jasstrong"]
DEPENDENCIES = ["sensor"]
UNIT_INDEX = "index"
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(
AQISensor,
unit_of_measurement=UNIT_INDEX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,

View File

@@ -1,8 +1,8 @@
#include "bh1750.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::bh1750 {
namespace esphome {
namespace bh1750 {
static const char *const TAG = "bh1750.sensor";
@@ -13,31 +13,6 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000;
static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f;
// Measurement time constants (datasheet values)
static constexpr uint16_t MTREG_DEFAULT = 69;
static constexpr uint16_t MTREG_MIN = 31;
static constexpr uint16_t MTREG_MAX = 254;
static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69
static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69
// Conversion constants (datasheet formulas)
static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor
static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution
// MTreg calculation constants
static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation)
static constexpr int COUNTS_NUMERATOR = 10;
static constexpr int COUNTS_DENOMINATOR = 12;
// MTreg register bit manipulation constants
static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5
static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits
static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0
static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits
/*
bh1750 properties:
@@ -68,7 +43,74 @@ void BH1750Sensor::setup() {
this->mark_failed();
return;
}
this->state_ = IDLE;
}
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
if (active_mtreg_ != mtreg) {
// set mtreg
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
}
active_mtreg_ = mtreg;
}
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = 24 * mtreg / 69;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = 180 * mtreg / 69;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = 180 * mtreg / 69;
break;
default:
f(NAN);
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
// probably not needed, but adjust for rounding
meas_time++;
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / 1.2f;
lx *= 69.0f / mtreg;
if (mode == BH1750_MODE_H2)
lx /= 2.0f;
f(lx);
});
}
void BH1750Sensor::dump_config() {
@@ -82,189 +124,45 @@ void BH1750Sensor::dump_config() {
}
void BH1750Sensor::update() {
const uint32_t now = millis();
// Start coarse measurement to determine optimal mode/mtreg
if (this->state_ != IDLE) {
// Safety timeout: reset if stuck
if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Measurement timeout, resetting state");
this->state_ = IDLE;
} else {
ESP_LOGW(TAG, "Previous measurement not complete, skipping update");
// first do a quick measurement in L-mode with full range
// to find right range
this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
}
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
this->state_ = WAITING_COARSE_MEASUREMENT;
this->enable_loop(); // Enable loop while measurement in progress
}
void BH1750Sensor::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case IDLE:
// Disable loop when idle to save cycles
this->disable_loop();
break;
case WAITING_COARSE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_COARSE_RESULT;
}
break;
case READING_COARSE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
this->process_coarse_result_(lx);
// Start fine measurement with optimal settings
// fetch millis() again since the read can take a bit
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) {
this->fail_and_reset_();
break;
}
this->state_ = WAITING_FINE_MEASUREMENT;
break;
BH1750Mode use_mode;
uint8_t use_mtreg;
if (val <= 7000) {
use_mode = BH1750_MODE_H2;
use_mtreg = 254;
} else {
use_mode = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
use_mtreg = std::min(254, std::max(31, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
case WAITING_FINE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_FINE_RESULT;
this->read_lx_(use_mode, use_mtreg, [this](float val) {
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
break;
case READING_FINE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
this->status_clear_warning();
this->publish_state(lx);
this->state_ = IDLE;
break;
}
}
}
bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) {
// Power on
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
return false;
}
// Set MTreg if changed
if (this->active_mtreg_ != mtreg) {
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
this->active_mtreg_ = 0;
return false;
}
this->active_mtreg_ = mtreg;
}
// Start measurement
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
default:
return false;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
return false;
}
// Store current measurement parameters
this->current_mode_ = mode;
this->current_mtreg_ = mtreg;
this->measurement_start_time_ = now;
this->measurement_duration_ = meas_time + 1; // Add 1ms for safety
return true;
}
bool BH1750Sensor::read_measurement_(float &lx_out) {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
return false;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / RESOLUTION_DIVISOR;
lx *= float(MTREG_DEFAULT) / this->current_mtreg_;
if (this->current_mode_ == BH1750_MODE_H2) {
lx /= MODE_H2_DIVISOR;
}
lx_out = lx;
return true;
}
void BH1750Sensor::process_coarse_result_(float lx) {
if (std::isnan(lx)) {
// Use defaults if coarse measurement failed
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
return;
}
if (lx <= HIGH_LIGHT_THRESHOLD_LX) {
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
} else {
this->fine_mode_ = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx);
this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_);
}
void BH1750Sensor::fail_and_reset_() {
this->status_set_warning();
this->publish_state(NAN);
this->state_ = IDLE;
this->publish_state(val);
});
});
}
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace esphome::bh1750
} // namespace bh1750
} // namespace esphome

View File

@@ -4,9 +4,10 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bh1750 {
namespace esphome {
namespace bh1750 {
enum BH1750Mode : uint8_t {
enum BH1750Mode {
BH1750_MODE_L,
BH1750_MODE_H,
BH1750_MODE_H2,
@@ -20,36 +21,13 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
float get_setup_priority() const override;
protected:
// State machine states
enum State : uint8_t {
IDLE,
WAITING_COARSE_MEASUREMENT,
READING_COARSE_RESULT,
WAITING_FINE_MEASUREMENT,
READING_FINE_RESULT,
};
void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
// 4-byte aligned members
uint32_t measurement_start_time_{0};
uint32_t measurement_duration_{0};
// 1-byte members grouped together to minimize padding
State state_{IDLE};
BH1750Mode current_mode_{BH1750_MODE_L};
uint8_t current_mtreg_{31};
BH1750Mode fine_mode_{BH1750_MODE_H2};
uint8_t fine_mtreg_{254};
uint8_t active_mtreg_{0};
// Helper methods
bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now);
bool read_measurement_(float &lx_out);
void process_coarse_result_(float lx);
void fail_and_reset_();
};
} // namespace esphome::bh1750
} // namespace bh1750
} // namespace esphome

View File

@@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
}
return false;

View File

@@ -1,23 +1,9 @@
"""
██╗ ██╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗
██║ ██║██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝
██║ █╗ ██║███████║██████╔╝██╔██╗ ██║██║██╔██╗ ██║██║ ███╗
██║███╗██║██╔══██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██║ ██║
╚███╔███╔╝██║ ██║██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝
╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝
AUTO-GENERATED FILE - DO NOT EDIT!
This file was auto-generated by libretiny/generate_components.py.
Any manual changes WILL BE LOST on regeneration.
To customize this component:
- Pin validators: Create gpio.py with validate_pin() or validate_usage()
- Schema extensions: Create schema.py with COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
Platform-specific code should be added to the main libretiny component
(__init__.py in esphome/components/libretiny/) rather than here.
"""
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
# For custom pin validators, put validate_pin() or validate_usage()
# in gpio.py file in this directory.
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
# in schema.py file in this directory.
from esphome import pins
from esphome.components import libretiny
@@ -41,7 +27,6 @@ COMPONENT_DATA = LibreTinyComponent(
board_pins=BK72XX_BOARD_PINS,
pin_validation=None,
usage_validation=None,
supports_atomics=False,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
import esphome.codegen as cg
from esphome.components.logger import request_log_listener
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
@@ -26,8 +25,5 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT_NUS", True)
expose_log = config[CONF_TYPE] == CONF_LOGS
cg.add(var.set_expose_log(expose_log))
if expose_log:
request_log_listener() # Request a log listener slot for BLE NUS log streaming
cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
await cg.register_component(var, config)

View File

@@ -50,7 +50,7 @@ TYPES = [
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(cg.EntityBase),
cv.GenerateID(): cv.declare_id(cg.Component),
cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,

View File

@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase
),
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"),
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(

View File

@@ -436,7 +436,7 @@ void Climate::save_state_() {
}
void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));

View File

@@ -153,7 +153,7 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

View File

@@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() {
this->send_command_(DALLAS_COMMAND_START_CONVERSION);
this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] {
this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] {
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
this->publish_state(NAN);
return;

View File

@@ -30,7 +30,7 @@ void DateEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);

View File

@@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_,
this->day_, this->hour_, this->minute_, this->second_);
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);

View File

@@ -26,7 +26,8 @@ void TimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_);
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);

View File

@@ -17,11 +17,7 @@ from esphome.const import (
UNIT_PERCENT,
)
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
from . import CONF_DEBUG_ID, DebugComponent
DEPENDENCIES = ["debug"]

View File

@@ -8,11 +8,7 @@ from esphome.const import (
ICON_RESTART,
)
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
from . import CONF_DEBUG_ID, DebugComponent
DEPENDENCIES = ["debug"]

View File

@@ -89,8 +89,10 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
delayMicroseconds(500);
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else {
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
delayMicroseconds(1000);
} else {
delayMicroseconds(800);
}
#ifdef USE_ESP32

View File

@@ -184,13 +184,12 @@ async def to_code(config):
height,
init_sequence_id,
init_sequence_length,
*model.get_constructor_args(config),
)
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await spi.register_spi_device(var, config, write_only=True)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -54,14 +54,20 @@ void EPaperBase::setup_pins_() const {
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
void EPaperBase::command(uint8_t value) {
ESP_LOGV(TAG, "Command: 0x%02X", value);
this->dc_pin_->digital_write(false);
this->enable();
this->start_command_();
this->write_byte(value);
this->disable();
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(uint8_t command, const uint8_t *ptr, size_t length) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(EPAPER_MAX_CMD_LOG_BYTES)];
@@ -124,10 +130,14 @@ void EPaperBase::wait_for_idle_(bool should_wait) {
void EPaperBase::loop() {
auto now = millis();
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0)
return;
if (this->delay_until_ != 0) {
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0) {
return;
}
this->delay_until_ = 0;
}
if (this->waiting_for_idle_) {
if (this->is_idle_()) {
this->waiting_for_idle_ = false;
@@ -182,7 +192,7 @@ void EPaperBase::process_state_() {
this->set_state_(EPaperState::RESET);
break;
case EPaperState::INITIALISE:
this->initialise(this->update_count_ != 0);
this->initialise_();
this->set_state_(EPaperState::TRANSFER_DATA);
break;
case EPaperState::TRANSFER_DATA:
@@ -220,11 +230,11 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
// allow subclasses to nominate delays
if (delay == 0)
delay = this->next_delay_;
this->next_delay_ = 0;
this->delay_until_ = millis() + delay;
if (delay != 0) {
this->delay_until_ = millis() + delay;
} else {
this->delay_until_ = 0;
}
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
if (state == EPaperState::IDLE) {
@@ -232,14 +242,22 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
}
}
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(bool partial) {
void EPaperBase::initialise_() {
size_t index = 0;
auto *sequence = this->init_sequence_;
@@ -299,8 +317,9 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
if (!rotate_coordinates_(x, y))
return;
const size_t byte_position = y * this->row_width_ + x / 8;
const uint8_t bit_position = x % 8;
const size_t pixel_position = y * this->width_ + x;
const size_t byte_position = pixel_position / 8;
const uint8_t bit_position = pixel_position % 8;
const uint8_t pixel_bit = 0x80 >> bit_position;
const auto original = this->buffer_[byte_position];
if ((color_to_bit(color) == 0)) {

View File

@@ -36,16 +36,14 @@ class EPaperBase : public Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
public:
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence = nullptr,
size_t init_sequence_length = 0, DisplayType display_type = DISPLAY_TYPE_BINARY)
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
: name_(name),
width_(width),
height_(height),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {
this->row_width_ = (this->width_ + 7) / 8; // width of a row in bytes
}
display_type_(display_type) {}
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; }
@@ -56,13 +54,9 @@ class EPaperBase : public Display,
void dump_config() override;
void command(uint8_t value);
void data(uint8_t value);
void cmd_data(uint8_t command, const uint8_t *ptr, size_t length);
// variant with in-place initializer list
void cmd_data(uint8_t command, std::initializer_list<uint8_t> data) {
this->cmd_data(command, data.begin(), data.size());
}
void update() override;
void loop() override;
@@ -115,7 +109,7 @@ class EPaperBase : public Display,
bool is_idle_() const;
void setup_pins_() const;
virtual bool reset();
virtual void initialise(bool partial);
void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
bool rotate_coordinates_(int &x, int &y);
@@ -149,12 +143,14 @@ class EPaperBase : public Display,
void set_state_(EPaperState state, uint16_t delay = 0);
void start_command_();
void end_command_();
void start_data_();
void end_data_();
// properties initialised in the constructor
const char *name_;
uint16_t width_;
uint16_t row_width_; // width of a row in bytes
uint16_t height_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
@@ -167,8 +163,7 @@ class EPaperBase : public Display,
GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{};
bool waiting_for_idle_{};
uint32_t delay_until_{}; // timestamp until which to delay processing
uint16_t next_delay_{}; // milliseconds to delay before next state
uint32_t delay_until_{};
uint8_t transform_{};
uint8_t update_count_{};
// these values represent the bounds of the updated buffer. Note that x_high and y_high

View File

@@ -80,17 +80,20 @@ void EPaperSpectraE6::power_on() {
void EPaperSpectraE6::power_off() {
ESP_LOGV(TAG, "Power off");
this->cmd_data(0x02, {0x00});
this->command(0x02);
this->data(0x00);
}
void EPaperSpectraE6::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh");
this->cmd_data(0x12, {0x00});
this->command(0x12);
this->data(0x00);
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->cmd_data(0x07, {0xA5});
this->command(0x07);
this->data(0xA5);
}
void EPaperSpectraE6::fill(Color color) {
@@ -140,7 +143,7 @@ bool HOT EPaperSpectraE6::transfer_data() {
if (buf_idx == sizeof bytes_to_send) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
this->end_data_();
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
@@ -154,7 +157,7 @@ bool HOT EPaperSpectraE6::transfer_data() {
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
this->end_data_();
}
this->current_data_index_ = 0;
return true;

View File

@@ -1,24 +1,25 @@
#include "epaper_spi_mono.h"
#include "epaper_spi_ssd1677.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.mono";
static constexpr const char *const TAG = "epaper_spi.ssd1677";
void EPaperMono::refresh_screen(bool partial) {
void EPaperSSD1677::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
this->command(0x22);
this->data(partial ? 0xFF : 0xF7);
this->command(0x20);
}
void EPaperMono::deep_sleep() {
void EPaperSSD1677::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->command(0x10);
}
bool EPaperMono::reset() {
bool EPaperSSD1677::reset() {
if (EPaperBase::reset()) {
this->command(0x12);
return true;
@@ -26,24 +27,29 @@ bool EPaperMono::reset() {
return false;
}
void EPaperMono::set_window() {
// round x-coordinates to byte boundaries
this->x_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_low_ / 256), (uint8_t) (this->x_high_ - 1),
(uint8_t) ((this->x_high_ - 1) / 256)});
this->cmd_data(0x4E, {(uint8_t) this->x_low_, (uint8_t) (this->x_low_ / 256)});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
}
bool HOT EPaperMono::transfer_data() {
bool HOT EPaperSSD1677::transfer_data() {
auto start_time = millis();
if (this->current_data_index_ == 0) {
uint8_t data[4]{};
// round to byte boundaries
this->set_window();
this->x_low_ &= ~7;
this->y_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
this->y_high_ += 7;
this->y_high_ &= ~7;
data[0] = this->x_low_;
data[1] = this->x_low_ / 256;
data[2] = this->x_high_ - 1;
data[3] = (this->x_high_ - 1) / 256;
cmd_data(0x4E, data, 2);
cmd_data(0x44, data, sizeof(data));
data[0] = this->y_low_;
data[1] = this->y_low_ / 256;
data[2] = this->y_high_ - 1;
data[3] = (this->y_high_ - 1) / 256;
cmd_data(0x4F, data, 2);
this->cmd_data(0x45, data, sizeof(data));
// for monochrome, we still need to clear the red data buffer at least once to prevent it
// causing dirty pixels after partial refresh.
this->command(this->send_red_ ? 0x26 : 0x24);
@@ -52,10 +58,10 @@ bool HOT EPaperMono::transfer_data() {
size_t row_length = (this->x_high_ - this->x_low_) / 8;
FixedVector<uint8_t> bytes_to_send{};
bytes_to_send.init(row_length);
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++];
}
@@ -63,12 +69,12 @@ bool HOT EPaperMono::transfer_data() {
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
this->end_data_();
return false;
}
}
this->disable();
this->end_data_();
this->current_data_index_ = 0;
if (this->send_red_) {
this->send_red_ = false;

View File

@@ -3,15 +3,13 @@
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* A class for monochrome epaper displays.
*/
class EPaperMono : public EPaperBase {
class EPaperSSD1677 : public EPaperBase {
public:
EPaperMono(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
EPaperSSD1677(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = (width + 7) / 8 * height; // 8 pixels per byte, rounded up
this->buffer_length_ = width * height / 8; // 8 pixels per byte
}
protected:
@@ -20,7 +18,6 @@ class EPaperMono : public EPaperBase {
void power_off() override{};
void deep_sleep() override;
bool reset() override;
virtual void set_window();
bool transfer_data() override;
bool send_red_{true};
};

View File

@@ -1,47 +0,0 @@
#include "epaper_waveshare.h"
namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi.waveshare";
void EpaperWaveshare::initialise(bool partial) {
EPaperBase::initialise(partial);
if (partial) {
this->cmd_data(0x32, this->partial_lut_, this->partial_lut_length_);
this->cmd_data(0x3C, {0x80});
this->cmd_data(0x22, {0xC0});
this->command(0x20);
this->next_delay_ = 100;
} else {
this->cmd_data(0x32, this->lut_, this->lut_length_);
this->cmd_data(0x3C, {0x05});
}
this->send_red_ = true;
}
void EpaperWaveshare::set_window() {
this->x_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
uint16_t x_start = this->x_low_ / 8;
uint16_t x_end = (this->x_high_ - 1) / 8;
this->cmd_data(0x44, {(uint8_t) x_start, (uint8_t) (x_end)});
this->cmd_data(0x4E, {(uint8_t) x_start});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
ESP_LOGV(TAG, "Set window X: %u-%u, Y: %u-%u", this->x_low_, this->x_high_, this->y_low_, this->y_high_);
}
void EpaperWaveshare::refresh_screen(bool partial) {
if (partial) {
this->cmd_data(0x22, {0x0F});
} else {
this->cmd_data(0x22, {0xC7});
}
this->command(0x20);
this->next_delay_ = partial ? 100 : 3000;
}
void EpaperWaveshare::deep_sleep() { this->cmd_data(0x10, {0x01}); }
} // namespace esphome::epaper_spi

View File

@@ -1,30 +0,0 @@
#pragma once
#include "epaper_spi.h"
#include "epaper_spi_mono.h"
namespace esphome::epaper_spi {
/**
* An epaper display that needs LUTs to be sent to it.
*/
class EpaperWaveshare : public EPaperMono {
public:
EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut,
uint16_t partial_lut_length)
: EPaperMono(name, width, height, init_sequence, init_sequence_length),
lut_(lut),
lut_length_(lut_length),
partial_lut_(partial_lut),
partial_lut_length_(partial_lut_length) {}
protected:
void initialise(bool partial) override;
void set_window() override;
void refresh_screen(bool partial) override;
void deep_sleep() override;
const uint8_t *lut_;
size_t lut_length_;
const uint8_t *partial_lut_;
uint16_t partial_lut_length_;
};
} // namespace esphome::epaper_spi

View File

@@ -32,9 +32,6 @@ class EpaperModel:
return cv.Required(name)
return cv.Optional(name, default=self.get_default(name, fallback))
def get_constructor_args(self, config) -> tuple:
return ()
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is

View File

@@ -4,9 +4,10 @@ from . import EpaperModel
class SSD1677(EpaperModel):
def __init__(self, name, class_name="EPaperMono", data_rate="20MHz", **defaults):
defaults[CONF_DATA_RATE] = data_rate
super().__init__(name, class_name, **defaults)
def __init__(self, name, class_name="EPaperSSD1677", **kwargs):
if CONF_DATA_RATE not in kwargs:
kwargs[CONF_DATA_RATE] = "20MHz"
super().__init__(name, class_name, **kwargs)
# fmt: off
def get_init_sequence(self, config: dict):
@@ -22,15 +23,11 @@ class SSD1677(EpaperModel):
ssd1677 = SSD1677("ssd1677")
wave_4_26 = ssd1677.extend(
"waveshare-4.26in",
ssd1677.extend(
"seeed-ee04-mono-4.26",
width=800,
height=480,
mirror_x=True,
)
wave_4_26.extend(
"seeed-ee04-mono-4.26",
cs_pin=44,
dc_pin=10,
reset_pin=38,

View File

@@ -1,88 +0,0 @@
import esphome.codegen as cg
from esphome.core import ID
from ..display import CONF_INIT_SEQUENCE_ID
from . import EpaperModel
class WaveshareModel(EpaperModel):
def __init__(self, name, lut, lut_partial=None, **defaults):
super().__init__(name, "EpaperWaveshare", **defaults)
self.lut = lut
self.lut_partial = lut_partial
def get_constructor_args(self, config) -> tuple:
lut = (
cg.static_const_array(
ID(config[CONF_INIT_SEQUENCE_ID].id + "_lut", type=cg.uint8), self.lut
),
len(self.lut),
)
if self.lut_partial is None:
lut_partial = cg.nullptr, 0
else:
lut_partial = (
cg.static_const_array(
ID(
config[CONF_INIT_SEQUENCE_ID].id + "_lut_partial", type=cg.uint8
),
self.lut_partial,
),
len(self.lut_partial),
)
return *lut, *lut_partial
# fmt: off
WaveshareModel(
"waveshare-2.13in-v3",
width=122,
height=250,
initsequence=(
(0x01, 0x27, 0x01, 0x00), # driver output control
(0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00),
(0x11, 0x03), # Data entry mode
(0x3F, 0x22), # Undocumented command
(0x2C, 0x36), # write VCOM register
(0x04, 0x41, 0x0C, 0x32), # SRC voltage
(0x03, 0x17), # Gate voltage
(0x21, 0x00, 0x80), # Display update control
(0x18, 0x80), # Select internal temperature sensor
),
lut=(
0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0,
0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x0, 0x0, 0x0,
),
lut_partial=(
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x0, 0x0, 0x0,
),
)

View File

@@ -180,12 +180,6 @@ def set_core_data(config):
path=[CONF_CPU_FREQUENCY],
)
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
"400MHz on ESP32-P4 is experimental and may not boot. "
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
)
CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK]

View File

@@ -193,18 +193,10 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_lifecycle_event_(const char *name) {
void BLEClientBase::log_gattc_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_data_event_(const char *name) {
// Data transfer events are logged at VERBOSE level because logging to UART creates
// delays that cause timing issues during time-sensitive BLE operations. This is
// especially problematic during pairing or firmware updates which require rapid
// writes to many characteristics - the log spam can cause these operations to fail.
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status);
}
@@ -288,7 +280,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr(param->open.remote_bda))
return false;
this->log_gattc_lifecycle_event_("OPEN");
this->log_gattc_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0;
@@ -339,7 +331,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr(param->connect.remote_bda))
return false;
this->log_gattc_lifecycle_event_("CONNECT");
this->log_gattc_event_("CONNECT");
this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
@@ -384,7 +376,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id)
return false;
this->log_gattc_lifecycle_event_("CLOSE");
this->log_gattc_event_("CLOSE");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
@@ -412,7 +404,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_gattc_lifecycle_event_("SEARCH_CMPL");
this->log_gattc_event_("SEARCH_CMPL");
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
@@ -439,35 +431,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_data_event_("READ_DESCR");
this->log_gattc_event_("READ_DESCR");
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_data_event_("WRITE_DESCR");
this->log_gattc_event_("WRITE_DESCR");
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_data_event_("WRITE_CHAR");
this->log_gattc_event_("WRITE_CHAR");
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id)
return false;
this->log_gattc_data_event_("READ_CHAR");
this->log_gattc_event_("READ_CHAR");
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id)
return false;
this->log_gattc_data_event_("NOTIFY");
this->log_gattc_event_("NOTIFY");
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_gattc_data_event_("REG_FOR_NOTIFY");
this->log_gattc_event_("REG_FOR_NOTIFY");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value
@@ -499,7 +491,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_err_t status =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) {
this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
}
@@ -507,13 +499,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
this->log_gattc_data_event_("UNREG_FOR_NOTIFY");
this->log_gattc_event_("UNREG_FOR_NOTIFY");
break;
}
default:
// Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations
ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
// ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
break;
}
return true;

View File

@@ -127,8 +127,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding
void log_event_(const char *name);
void log_gattc_lifecycle_event_(const char *name);
void log_gattc_data_event_(const char *name);
void log_gattc_event_(const char *name);
void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,

View File

@@ -12,7 +12,6 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -43,7 +42,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",

View File

@@ -8,10 +8,8 @@
#include <esp_app_desc.h>
#include <esp_hosted.h>
#include <esp_hosted_host_fw_ver.h>
#include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
@@ -182,23 +180,15 @@ bool Esp32HostedUpdate::fetch_manifest_() {
}
// Read manifest JSON into string (manifest is small, ~1KB max)
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -303,38 +293,33 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
sha256::SHA256 hasher;
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < total_size) {
int read_or_error = container->read(buffer, sizeof(buffer));
int read = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");
} else {
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
// read == 0: no more data available, exit loop
break;
}
hasher.add(buffer, read_or_error);
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
@@ -366,7 +351,8 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
}
// Verify SHA256 before writing
sha256::SHA256 hasher;
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
@@ -456,12 +442,6 @@ void Esp32HostedUpdate::perform(bool force) {
this->status_clear_error();
this->publish_state();
#ifdef USE_OTA_ROLLBACK
// Mark the host partition as valid before rebooting, in case the safe mode
// timer hasn't expired yet.
esp_ota_mark_app_valid_cancel_rollback();
#endif
// Schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });

View File

@@ -370,14 +370,12 @@ void ESPHomeOTAComponent::handle_data_() {
error:
this->write_byte_(static_cast<uint8_t>(error_code));
this->cleanup_connection_();
// Abort backend before cleanup - cleanup_connection_() destroys the backend
if (this->backend_ != nullptr && update_started) {
this->backend_->abort();
}
this->cleanup_connection_();
this->status_momentary_error("err", 5000);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
@@ -563,9 +561,11 @@ bool ESPHomeOTAComponent::handle_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
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
sha256::SHA256 hasher;
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4;
@@ -637,9 +637,11 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size;
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
sha256::SHA256 hasher;
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
hasher.init();
hasher.add(this->password_.c_str(), this->password_.length());

View File

@@ -61,21 +61,6 @@ DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["network"]
LOGGER = logging.getLogger(__name__)
# Key for tracking IP state listener count in CORE.data
ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners"
def request_ethernet_ip_state_listener() -> None:
"""Request an IP state listener slot.
Components that implement EthernetIPStateListener should call this
in their to_code() to register for IP state notifications.
"""
CORE.data[ETHERNET_IP_STATE_LISTENERS_KEY] = (
CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0) + 1
)
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
# These pins are used by the internal Ethernet MAC when using RMII PHYs
ESP32_RMII_FIXED_PINS = {
@@ -426,8 +411,6 @@ async def to_code(config):
if CORE.using_arduino:
cg.add_library("WiFi", None)
CORE.add_job(final_step)
def _final_validate_rmii_pins(config: ConfigType) -> None:
"""Validate that RMII pins are not used by other components."""
@@ -484,11 +467,3 @@ def _final_validate(config: ConfigType) -> ConfigType:
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional Ethernet features."""
if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0):
cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS")
cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count)

View File

@@ -472,12 +472,6 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#endif
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
@@ -504,9 +498,6 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
@@ -523,23 +514,9 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::notify_ip_state_listeners_() {
auto ips = this->get_ip_addresses();
auto dns1 = this->get_dns_address(0);
auto dns2 = this->get_dns_address(1);
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(ips, dns1, dns2);
}
}
#endif // USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect

View File

@@ -17,22 +17,6 @@
namespace esphome {
namespace ethernet {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
/** Listener interface for Ethernet IP state changes.
*
* Components can implement this interface to receive IP address updates
* without the overhead of std::function callbacks or polling.
*
* @note Components must call ethernet.request_ethernet_ip_state_listener() in their
* Python to_code() to register for this listener type.
*/
class EthernetIPStateListener {
public:
virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) = 0;
};
#endif // USE_ETHERNET_IP_STATE_LISTENERS
enum EthernetType : uint8_t {
ETHERNET_TYPE_UNKNOWN = 0,
ETHERNET_TYPE_LAN8720,
@@ -115,19 +99,12 @@ class EthernetComponent : public Component {
eth_speed_t get_link_speed();
bool powerdown();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
#endif
protected:
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#if LWIP_IPV6
static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#endif /* LWIP_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
void start_connect_();
void finish_connect_();
@@ -186,10 +163,6 @@ class EthernetComponent : public Component {
esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
#endif
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.

View File

@@ -7,44 +7,8 @@ namespace esphome::ethernet_info {
static const char *const TAG = "ethernet_info";
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void IPAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); }
void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) {
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
uint8_t sensor = 0;
for (const auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
}
}
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
void DNSAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo DNS Address", this); }
void DNSAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) {
// IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot
char buf[network::IP_ADDRESS_BUFFER_SIZE * 2];
dns1.str_to(buf);
size_t len1 = strlen(buf);
buf[len1] = ' ';
dns2.str_to(buf + len1 + 1);
this->publish_state(buf);
}
#endif // USE_ETHERNET_IP_STATE_LISTENERS
void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo MAC Address", this); }
} // namespace esphome::ethernet_info

View File

@@ -8,37 +8,64 @@
namespace esphome::ethernet_info {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
class IPAddressEthernetInfo final : public Component,
public text_sensor::TextSensor,
public ethernet::EthernetIPStateListener {
class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
public:
void setup() override;
void update() override {
auto ips = ethernet::global_eth_component->get_ip_addresses();
if (ips != this->last_ips_) {
this->last_ips_ = ips;
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
uint8_t sensor = 0;
for (auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
}
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
network::IPAddresses last_ips_;
std::array<text_sensor::TextSensor *, 5> ip_sensors_;
};
class DNSAddressEthernetInfo final : public Component,
public text_sensor::TextSensor,
public ethernet::EthernetIPStateListener {
class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
public:
void setup() override;
void update() override {
auto dns1 = ethernet::global_eth_component->get_dns_address(0);
auto dns2 = ethernet::global_eth_component->get_dns_address(1);
if (dns1 != this->last_dns1_ || dns2 != this->last_dns2_) {
this->last_dns1_ = dns1;
this->last_dns2_ = dns2;
// IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot
char buf[network::IP_ADDRESS_BUFFER_SIZE * 2];
dns1.str_to(buf);
size_t len1 = strlen(buf);
buf[len1] = ' ';
dns2.str_to(buf + len1 + 1);
this->publish_state(buf);
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
void dump_config() override;
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
network::IPAddress last_dns1_;
network::IPAddress last_dns2_;
};
#endif // USE_ETHERNET_IP_STATE_LISTENERS
class MACAddressEthernetInfo final : public Component, public text_sensor::TextSensor {
class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor {
public:
void setup() override {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import ethernet, text_sensor
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_DNS_ADDRESS,
@@ -13,22 +13,24 @@ DEPENDENCIES = ["ethernet"]
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
IPAddressEthernetInfo = ethernet_info_ns.class_(
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
)
DNSAddressEthernetInfo = ethernet_info_ns.class_(
"DNSAddressEthernetInfo", text_sensor.TextSensor, cg.Component
"DNSAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
)
MACAddressEthernetInfo = ethernet_info_ns.class_(
"MACAddressEthernetInfo", text_sensor.TextSensor, cg.Component
"MACAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema(
IPAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(
)
.extend(cv.polling_component_schema("1s"))
.extend(
{
cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
@@ -38,7 +40,7 @@ CONFIG_SCHEMA = cv.Schema(
),
cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema(
DNSAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
).extend(cv.polling_component_schema("1s")),
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
MACAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
@@ -47,12 +49,6 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
# Request Ethernet IP state listener slots - one per sensor type
if CONF_IP_ADDRESS in config:
ethernet.request_ethernet_ip_state_listener()
if CONF_DNS_ADDRESS in config:
ethernet.request_ethernet_ip_state_listener()
if conf := config.get(CONF_IP_ADDRESS):
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
@@ -61,8 +57,8 @@ async def to_code(config):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(ip_info.add_ip_sensors(x, sens))
if conf := config.get(CONF_DNS_ADDRESS):
dns_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(dns_info, conf)
dns_info = await text_sensor.new_text_sensor(config[CONF_DNS_ADDRESS])
await cg.register_component(dns_info, config[CONF_DNS_ADDRESS])
if conf := config.get(CONF_MAC_ADDRESS):
mac_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(mac_info, conf)
mac_info = await text_sensor.new_text_sensor(config[CONF_MAC_ADDRESS])
await cg.register_component(mac_info, config[CONF_MAC_ADDRESS])

View File

@@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) {
return;
}
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type);
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);

View File

@@ -1,7 +1,6 @@
#pragma once
#include <cstring>
#include <limits>
#include <string>
#include <vector>
@@ -49,24 +48,6 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Return the last triggered event type, or empty StringRef if no event triggered yet.
StringRef get_last_event_type() const { return StringRef::from_maybe_nullptr(this->last_event_type_); }
/// Return event type by index, or nullptr if index is out of bounds.
const char *get_event_type(uint8_t index) const {
return index < this->types_.size() ? this->types_[index] : nullptr;
}
/// Return index of last triggered event type, or max uint8_t if no event triggered yet.
uint8_t get_last_event_type_index() const {
if (this->last_event_type_ == nullptr)
return std::numeric_limits<uint8_t>::max();
// Most events have <3 types, uint8_t is sufficient for all reasonable scenarios
const uint8_t size = static_cast<uint8_t>(this->types_.size());
for (uint8_t i = 0; i < size; i++) {
if (this->types_[i] == this->last_event_type_)
return i;
}
return std::numeric_limits<uint8_t>::max();
}
/// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; }

View File

@@ -201,7 +201,7 @@ void Fan::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
"'%s' >>\n"
"'%s' - Sending state:\n"
" State: %s",
this->name_.c_str(), ONOFF(this->state));
if (traits.supports_speed()) {

View File

@@ -1,5 +1,4 @@
#include "fingerprint_grow.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -533,21 +532,14 @@ void FingerprintGrowComponent::sensor_sleep_() {
}
void FingerprintGrowComponent::dump_config() {
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
if (this->has_sensing_pin_) {
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
}
if (this->has_power_pin_) {
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
}
ESP_LOGCONFIG(TAG,
"GROW_FINGERPRINT_READER:\n"
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
this->system_identifier_code_,
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {

View File

@@ -1,3 +1,4 @@
#include <cstdio>
#include <cstring>
#include "hmac_sha256.h"
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
@@ -25,7 +26,9 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
void HmacSHA256::get_hex(char *output) {
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE);
for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
sprintf(output + (i * 2), "%02x", this->digest_[i]);
}
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {

View File

@@ -157,7 +157,6 @@ async def to_code(config):
if CORE.is_esp32:
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
if config.get(CONF_VERIFY_SSL):
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)

View File

@@ -79,81 +79,6 @@ inline bool is_redirect(int const status) {
*/
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
/*
* HTTP Container Read Semantics
* =============================
*
* IMPORTANT: These semantics differ from standard BSD sockets!
*
* BSD socket read() returns:
* > 0: bytes read
* == 0: connection closed (EOF)
* < 0: error (check errno)
*
* HttpContainer::read() returns:
* > 0: bytes read successfully
* == 0: no data available yet OR all content read
* (caller should check bytes_read vs content_length)
* < 0: error or connection closed (caller should EXIT)
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
* other negative values = platform-specific errors
*
* Platform behaviors:
* - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
*/
/// Error code returned by HttpContainer::read() when connection closed prematurely
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
/// Status of a read operation
enum class HttpReadStatus : uint8_t {
OK, ///< Read completed successfully
ERROR, ///< Read error occurred
TIMEOUT, ///< Timeout waiting for data
};
/// Result of an HTTP read operation
struct HttpReadResult {
HttpReadStatus status; ///< Status of the read operation
int error_code; ///< Error code from read() on failure, 0 on success
};
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
if (bytes_read_or_error > 0) {
last_data_time = millis();
return HttpReadLoopResult::DATA;
}
if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR;
}
// bytes_read_or_error == 0: no data available yet
if (millis() - last_data_time >= timeout_ms) {
return HttpReadLoopResult::TIMEOUT;
}
delay(1); // Small delay to prevent tight spinning
return HttpReadLoopResult::RETRY;
}
class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
@@ -163,33 +88,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
int status_code;
uint32_t duration_ms;
/**
* @brief Read data from the HTTP response body.
*
* WARNING: These semantics differ from BSD sockets!
* BSD sockets: 0 = EOF (connection closed)
* This method: 0 = no data yet OR all content read, negative = error/closed
*
* @param buf Buffer to read data into
* @param max_len Maximum number of bytes to read
* @return
* - > 0: Number of bytes read successfully
* - 0: No data available yet OR all content read
* (check get_bytes_read() >= content_length to distinguish)
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
* - < -1: Other error (platform-specific error code)
*
* Platform notes:
* - ESP-IDF: blocking read, 0 only when all content read
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
*
* Use get_bytes_read() and content_length to track progress.
* When get_bytes_read() >= content_length, all data has been received.
*
* IMPORTANT: Do not use raw return values directly. Use these helpers:
* - http_read_loop_result(): for loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes" operations
*/
virtual int read(uint8_t *buf, size_t max_len) = 0;
virtual void end() = 0;
@@ -212,38 +110,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
std::map<std::string, std::list<std::string>> response_headers_{};
};
/// Read data from HTTP container into buffer with timeout handling
/// Handles feed_wdt, yield, and timeout checking internally
/// @param container The HTTP container to read from
/// @param buffer Buffer to read into
/// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) {
size_t read_index = 0;
uint32_t last_data_time = millis();
while (read_index < total_size) {
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT)
return {HttpReadStatus::TIMEOUT, 0};
read_index += read_bytes_or_error;
}
return {HttpReadStatus::OK, 0};
}
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public:
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
@@ -258,7 +124,6 @@ class HttpRequestComponent : public Component {
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
uint32_t get_timeout() const { return this->timeout_; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -377,28 +242,24 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
return;
}
size_t max_length = this->max_response_buffer_size_;
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
std::string response_body;
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < max_length) {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
read_index += read_or_error;
read_index += read;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);

View File

@@ -131,10 +131,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
}
}
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
// early return check (bytes_read_ >= content_length) will never trigger.
int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length;
@@ -143,23 +139,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
return container;
}
// Arduino HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
// no data is ready. We use connected() to distinguish "no data yet" from
// "connection closed".
//
// WiFiClient behavior:
// available() > 0: data ready to read
// available() == 0 && connected(): no data yet, still connected
// available() == 0 && !connected(): connection closed
//
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
// > 0: bytes read
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
// < 0: error/connection closed <-- connection closed returns -1, not 0
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
@@ -167,29 +146,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return HTTP_ERROR_CONNECTION_CLOSED;
return -1;
}
int available_data = stream_ptr->available();
// For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data));
if (bufsize == 0) {
this->duration_ms += (millis() - start);
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
// For chunked encoding, !connected() after reading means EOF (all chunks received)
// For known content_length with bytes_read_ < content_length, it means connection dropped
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
}
return 0; // No data yet, caller should retry
return 0;
}
App.feed_wdt();

View File

@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure && this->verify_ssl_) {
if (secure) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
#endif
@@ -152,8 +152,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
// The read() method handles content_length == 0 specially to support chunked responses.
container->content_length = esp_http_client_fetch_headers(client);
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
@@ -211,71 +209,32 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
// ESP-IDF HTTP read implementation (blocking mode)
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// esp_http_client_read() in blocking mode returns:
// > 0: bytes read
// 0: connection closed (end of stream)
// < 0: error
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: all content read (only returned when content_length is known and fully read)
// < 0: error/connection closed
//
// Note on chunked transfer encoding:
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
// We handle this by skipping the content_length check when content_length is 0,
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
// by returning 0.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
// For chunked responses, esp_http_client_read() will return 0 when all data is received
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
}
this->feed_wdt();
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
if (read_len_or_error > 0) {
this->bytes_read_ += read_len_or_error;
return read_len_or_error;
}
// esp_http_client_read() returns 0 in two cases:
// 1. Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// For case 2, 0 indicates that all chunked data has already been delivered
// in previous successful read() calls, so treating this as a closed
// connection does not cause any loss of response data.
if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED;
}
// Negative value - error, return the actual error code for debugging
return read_len_or_error;
return read_len;
}
void HttpContainerIDF::end() {
if (this->client_ == nullptr) {
return; // Already cleaned up
}
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
this->client_ = nullptr;
}
void HttpContainerIDF::feed_wdt() {

View File

@@ -34,7 +34,6 @@ class HttpRequestIDF : public HttpRequestComponent {
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
@@ -43,7 +42,6 @@ class HttpRequestIDF : public HttpRequestComponent {
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
bool verify_ssl_{true};
/// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt);

View File

@@ -115,47 +115,39 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
return error_code;
}
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
container->content_length, bufsize_or_error);
// read a maximum of chunk_size bytes into buf. (real read size returned)
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
container->content_length, bufsize);
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA) {
if (result == HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data");
} else {
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
// Exit loop if no data available (stream closed or end of data)
if (bufsize <= 0) {
if (bufsize < 0) {
ESP_LOGE(TAG, "Stream closed with error");
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
}
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
// bufsize == 0: no more data available, exit loop
break;
}
// At this point bufsize_or_error > 0, so it's a valid size
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// add read bytes to MD5
md5_receive.add(buf, bufsize_or_error);
md5_receive.add(buf, bufsize);
// write bytes to OTA backend
this->update_started_ = true;
error_code = backend->write(buf, bufsize_or_error);
error_code = backend->write(buf, bufsize);
if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
container->get_bytes_read() - bufsize_or_error, container->content_length);
container->get_bytes_read() - bufsize, container->content_length);
this->cleanup_(std::move(backend), container);
return error_code;
}
@@ -252,19 +244,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
}
this->md5_expected_.resize(MD5_SIZE);
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
this->parent_->get_timeout());
int read_len = 0;
while (container->get_bytes_read() < MD5_SIZE) {
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
if (read_len <= 0) {
break;
}
App.feed_wdt();
yield();
}
container->end();
if (result.status != HttpReadStatus::OK) {
if (result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading MD5");
} else {
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
}
return false;
}
return true;
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
return read_len == MD5_SIZE;
}
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {

View File

@@ -11,12 +11,7 @@ namespace http_request {
// The update function runs in a task only on ESP32s.
#ifdef USE_ESP32
// vTaskDelete doesn't return, but clang-tidy doesn't know that
#define UPDATE_RETURN \
do { \
vTaskDelete(nullptr); \
__builtin_unreachable(); \
} while (0)
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
#else
#define UPDATE_RETURN return
#endif
@@ -75,21 +70,19 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
yield();
if (read_bytes <= 0) {
// Network error or connection closed - break to avoid infinite loop
break;
}
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
allocator.deallocate(data, container->content_length);
container->end();
UPDATE_RETURN;
read_index += read_bytes;
}
size_t read_index = container->get_bytes_read();
bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends

View File

@@ -1,4 +1,3 @@
import logging
from typing import Any
from esphome import automation, pins
@@ -19,16 +18,13 @@ from esphome.const import (
CONF_ROTATION,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID, EnumValue
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import add_class_to_obj
from esphome.types import ConfigType
from . import boards, hub75_ns
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@stuartparmenter"]
@@ -124,51 +120,13 @@ PANEL_LAYOUTS = {
}
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_WIRINGS = {
SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
}
# Deprecated scan wiring names - mapped to new names
DEPRECATED_SCAN_WIRINGS = {
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
}
def _validate_scan_wiring(value):
"""Validate scan_wiring with deprecation warnings for old names."""
value = cv.string(value).upper().replace(" ", "_")
# Check if using deprecated name
# Remove deprecated names in 2026.7.0
if value in DEPRECATED_SCAN_WIRINGS:
new_name = DEPRECATED_SCAN_WIRINGS[value]
_LOGGER.warning(
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
"Please use '%s' instead.",
value,
new_name,
)
value = new_name
# Validate against allowed values
if value not in SCAN_WIRINGS:
raise cv.Invalid(
f"Unknown scan wiring '{value}'. "
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
)
# Return as EnumValue like cv.enum does
result = add_class_to_obj(value, EnumValue)
result.enum_value = SCAN_WIRINGS[value]
return result
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
CLOCK_SPEEDS = {
"8MHZ": Hub75ClockSpeed.HZ_8M,
@@ -424,7 +382,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
# Panel hardware configuration
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring,
cv.Optional(CONF_SCAN_WIRING): cv.enum(
SCAN_PATTERNS, upper=True, space="_"
),
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
# Display configuration
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
@@ -587,7 +547,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.3.0",
ref="0.2.2",
)
# Set compile-time configuration via build flags (so external library sees them)

View File

@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
}
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get();
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get(len + 1);
buffer[0] = a_register;
std::copy(data, data + len, buffer + 1);
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
}
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get();
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get(len + 2);
buffer[0] = a_register >> 8;
buffer[1] = a_register;

View File

@@ -11,6 +11,22 @@
namespace esphome {
namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> heap_buffer_;
};
/// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode {
NO_ERROR = 0, ///< No error found during execution of method
@@ -76,8 +92,8 @@ class I2CBus {
total_len += read_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get();
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get(total_len);
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
if (err != ERROR_OK)
@@ -100,8 +116,8 @@ class I2CBus {
total_len += write_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get();
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get(total_len);
size_t pos = 0;
for (size_t i = 0; i != count; i++) {

View File

@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
}
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;

View File

@@ -340,8 +340,8 @@ void I2SAudioSpeaker::speaker_task(void *params) {
const uint32_t read_delay =
(this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read;
if (bytes_read > 0) {
if (this_speaker->q15_volume_factor_ < INT16_MAX) {

View File

@@ -223,7 +223,7 @@ async def to_code(config):
var = cg.Pvariable(config[CONF_ID], rhs)
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=True)
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 init_sequences := config.get(CONF_INIT_SEQUENCE):

View File

@@ -665,10 +665,15 @@ async def write_image(config, all_frames=False):
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
if resize:
width, height = resize
# resvg-py allows rendering by width/height directly
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))

View File

@@ -193,12 +193,8 @@ std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv:
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
snprintf(webserver_url, sizeof(webserver_url), "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
urls.emplace_back(webserver_url);
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}

View File

@@ -1,11 +0,0 @@
"""IR/RF Proxy component - provides remote_base backend for infrared platform."""
import esphome.codegen as cg
CODEOWNERS = ["@kbx81"]
# Namespace and constants exported for infrared.py platform
ir_rf_proxy_ns = cg.esphome_ns.namespace("ir_rf_proxy")
CONF_REMOTE_RECEIVER_ID = "remote_receiver_id"
CONF_REMOTE_TRANSMITTER_ID = "remote_transmitter_id"

View File

@@ -1,77 +0,0 @@
"""Infrared platform implementation using remote_base (remote_transmitter/receiver)."""
from typing import Any
import esphome.codegen as cg
from esphome.components import infrared, remote_receiver, remote_transmitter
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["infrared"]
IrRfProxy = ir_rf_proxy_ns.class_("IrRfProxy", infrared.Infrared)
CONFIG_SCHEMA = cv.All(
infrared.infrared_schema(IrRfProxy).extend(
{
cv.Optional(CONF_FREQUENCY, default=0): cv.frequency,
cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent
),
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
}
),
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
)
def _final_validate(config: dict[str, Any]) -> None:
"""Validate that transmitters have a proper carrier duty cycle."""
# Only validate if this is an infrared (not RF) configuration with a transmitter
if config.get(CONF_FREQUENCY, 0) != 0 or CONF_REMOTE_TRANSMITTER_ID not in config:
return
# Get the transmitter configuration
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
full_config = fv.full_config.get()
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
transmitter_config = full_config.get_config_for_path(transmitter_path)
# Check if carrier_duty_percent set to 0 or 100
# Note: remote_transmitter schema requires this field and validates 1-100%,
# but we double-check here for infrared to provide a helpful error message
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
if duty_percent in {0, 100}:
raise cv.Invalid(
f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' configured with "
"an intermediate value (typically 30-50%) for infrared transmission. If this is an RF "
f"transmitter, configure this infrared with a '{CONF_FREQUENCY}' value greater than 0"
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: dict[str, Any]) -> None:
"""Code generation for remote_base infrared platform."""
# Create and register the infrared entity
var = await infrared.new_infrared(config)
# Set frequency / 1000; zero indicates infrared hardware
cg.add(var.set_frequency(config[CONF_FREQUENCY] / 1000))
# Link transmitter if specified
if CONF_REMOTE_TRANSMITTER_ID in config:
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))
# Link receiver if specified
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))

View File

@@ -1,23 +0,0 @@
#include "ir_rf_proxy.h"
#include "esphome/core/log.h"
namespace esphome::ir_rf_proxy {
static const char *const TAG = "ir_rf_proxy";
void IrRfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"IR/RF Proxy '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
if (this->is_rf()) {
ESP_LOGCONFIG(TAG, " Hardware Type: RF (%.3f MHz)", this->frequency_khz_ / 1e3f);
} else {
ESP_LOGCONFIG(TAG, " Hardware Type: Infrared");
}
}
} // namespace esphome::ir_rf_proxy

View File

@@ -1,30 +0,0 @@
#pragma once
// WARNING: This component is EXPERIMENTAL. The API may change at any time
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/infrared/infrared.h"
namespace esphome::ir_rf_proxy {
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
class IrRfProxy : public infrared::Infrared {
public:
IrRfProxy() = default;
void dump_config() override;
/// Set RF frequency in kHz (0 = infrared, non-zero = RF)
void set_frequency(uint32_t frequency_khz) { this->frequency_khz_ = frequency_khz; }
/// Get RF frequency in kHz
uint32_t get_frequency() const { return this->frequency_khz_; }
/// Check if this is RF mode (non-zero frequency)
bool is_rf() const { return this->frequency_khz_ > 0; }
protected:
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
};
} // namespace esphome::ir_rf_proxy

View File

@@ -5,7 +5,6 @@ from esphome.const import (
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
CONF_ID,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_PRESENCE,
@@ -20,7 +19,6 @@ DEPENDENCIES = ["ld2410"]
CONF_OUT_PIN_PRESENCE_STATUS = "out_pin_presence_status"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,

View File

@@ -3,7 +3,6 @@ from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_FACTORY_RESET,
CONF_ID,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
@@ -22,7 +21,6 @@ RestartButton = ld2410_ns.class_("RestartButton", button.Button)
CONF_QUERY_PARAMS = "query_params"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,

View File

@@ -31,7 +31,6 @@ TIMEOUT_GROUP = "timeout"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema(
MaxDistanceTimeoutNumber,

View File

@@ -3,7 +3,6 @@ from esphome.components import select
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_ID,
ENTITY_CATEGORY_CONFIG,
ICON_LIGHTBULB,
ICON_RULER,
@@ -23,7 +22,6 @@ CONF_OUT_PIN_LEVEL = "out_pin_level"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema(
DistanceResolutionSelect,

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_LIGHT,
CONF_MOVING_DISTANCE,
DEVICE_CLASS_DISTANCE,
@@ -29,7 +28,6 @@ CONF_STILL_ENERGY = "still_energy"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,

View File

@@ -3,7 +3,6 @@ from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import (
CONF_BLUETOOTH,
CONF_ID,
DEVICE_CLASS_SWITCH,
ENTITY_CATEGORY_CONFIG,
ICON_BLUETOOTH,
@@ -18,7 +17,6 @@ EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch)
CONF_ENGINEERING_MODE = "engineering_mode"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema(
EngineeringModeSwitch,

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_MAC_ADDRESS,
CONF_VERSION,
ENTITY_CATEGORY_DIAGNOSTIC,
@@ -15,7 +14,6 @@ from . import CONF_LD2410_ID, LD2410Component
DEPENDENCIES = ["ld2410"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP

View File

@@ -5,7 +5,6 @@ from esphome.const import (
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
CONF_ID,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_RUNNING,
@@ -21,7 +20,6 @@ DEPENDENCIES = ["ld2412"]
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS = "dynamic_background_correction_status"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS

View File

@@ -3,7 +3,6 @@ from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_FACTORY_RESET,
CONF_ID,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
@@ -27,7 +26,6 @@ CONF_QUERY_PARAMS = "query_params"
CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,

View File

@@ -31,7 +31,6 @@ TIMEOUT_GROUP = "timeout"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
LightThresholdNumber,

View File

@@ -3,7 +3,6 @@ from esphome.components import select
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_ID,
ENTITY_CATEGORY_CONFIG,
ICON_LIGHTBULB,
ICON_RULER,
@@ -23,7 +22,6 @@ CONF_OUT_PIN_LEVEL = "out_pin_level"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_BAUD_RATE): select.select_schema(
BaudRateSelect,

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_LIGHT,
CONF_MOVING_DISTANCE,
DEVICE_CLASS_DISTANCE,
@@ -29,7 +28,6 @@ CONF_STILL_ENERGY = "still_energy"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,

View File

@@ -3,7 +3,6 @@ from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import (
CONF_BLUETOOTH,
CONF_ID,
DEVICE_CLASS_SWITCH,
ENTITY_CATEGORY_CONFIG,
ICON_BLUETOOTH,
@@ -18,7 +17,6 @@ EngineeringModeSwitch = LD2412_ns.class_("EngineeringModeSwitch", switch.Switch)
CONF_ENGINEERING_MODE = "engineering_mode"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_BLUETOOTH): switch.switch_schema(
BluetoothSwitch,

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_MAC_ADDRESS,
CONF_VERSION,
ENTITY_CATEGORY_DIAGNOSTIC,
@@ -15,7 +14,6 @@ from . import CONF_LD2412_ID, LD2412Component
DEPENDENCIES = ["ld2412"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP

View File

@@ -5,7 +5,6 @@ from esphome.const import (
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
CONF_ID,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
)
@@ -19,7 +18,6 @@ ICON_SHIELD_ACCOUNT = "mdi:shield-account"
ICON_TARGET_ACCOUNT = "mdi:target-account"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,

View File

@@ -3,7 +3,6 @@ from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_FACTORY_RESET,
CONF_ID,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
@@ -18,7 +17,6 @@ FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button)
RestartButton = ld2450_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,

View File

@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
float angle = 0;
int16_t angle = 0;
uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false;

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