Merge remote-tracking branch 'upstream/dev' into memory

This commit is contained in:
J. Nick Koston 2025-07-09 09:37:48 -10:00
commit 073590124d
No known key found for this signature in database
201 changed files with 8143 additions and 2125 deletions

View File

@ -214,17 +214,51 @@ jobs:
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
./venv/Scripts/activate ./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native -n auto tests pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest - name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: | run: |
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3 uses: codecov/codecov-action@v5.4.3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
integration-tests:
name: Run integration tests
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
run: |
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
clang-format: clang-format:
name: Check clang-format name: Check clang-format
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -494,6 +528,7 @@ jobs:
- flake8 - flake8
- pylint - pylint
- pytest - pytest
- integration-tests
- pyupgrade - pyupgrade
- clang-tidy - clang-tidy
- list-components - list-components

View File

@ -170,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz
@ -254,6 +255,7 @@ esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita
@ -442,6 +444,7 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337 esphome/components/sx127x/* @swoboda1337
esphome/components/syslog/* @clydebarrow esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes

View File

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

View File

@ -10,8 +10,15 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.const import (
CONF_ANALOG,
CONF_INPUT,
CONF_NUMBER,
PLATFORM_ESP8266,
PlatformFramework,
)
from esphome.core import CORE from esphome.core import CORE
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@ -229,3 +236,20 @@ def validate_adc_pin(value):
)(value) )(value)
raise NotImplementedError raise NotImplementedError
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"adc_sensor_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"adc_sensor_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -23,7 +23,7 @@ void APDS9960::setup() {
return; return;
} }
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->error_code_ = WRONG_ID; this->error_code_ = WRONG_ID;
this->mark_failed(); this->mark_failed();
return; return;

View File

@ -3,6 +3,7 @@ import base64
from esphome import automation from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config_helpers import get_logger_level
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTION, CONF_ACTION,
@ -313,3 +314,17 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
@automation.register_condition("api.connected", APIConnectedCondition, {}) @automation.register_condition("api.connected", APIConnectedCondition, {})
async def api_connected_to_code(config, condition_id, template_arg, args): async def api_connected_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg) return cg.new_Pvariable(condition_id, template_arg)
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled."""
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read
# all the way to the end even when ifdef'd out
#
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
# which happens when the logger level is VERY_VERBOSE
if get_logger_level() != "VERY_VERBOSE":
return ["api_pb2_dump.cpp"]
return []

View File

@ -42,6 +42,19 @@ static const char *const TAG = "api.connection";
static const int CAMERA_STOP_STREAM = 5000; static const int CAMERA_STOP_STREAM = 5000;
#endif #endif
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object
#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if ((entity_var) == nullptr) \
return; \
auto call = (entity_var)->make_call();
// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found
#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if ((entity_var) == nullptr) \
return;
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
@ -361,11 +374,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::cover_command(const CoverCommandRequest &msg) { void APIConnection::cover_command(const CoverCommandRequest &msg) {
cover::Cover *cover = App.get_cover_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
if (cover == nullptr)
return;
auto call = cover->make_call();
if (msg.has_legacy_command) { if (msg.has_legacy_command) {
switch (msg.legacy_command) { switch (msg.legacy_command) {
case enums::LEGACY_COVER_COMMAND_OPEN: case enums::LEGACY_COVER_COMMAND_OPEN:
@ -427,11 +436,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::fan_command(const FanCommandRequest &msg) {
fan::Fan *fan = App.get_fan_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
if (fan == nullptr)
return;
auto call = fan->make_call();
if (msg.has_state) if (msg.has_state)
call.set_state(msg.state); call.set_state(msg.state);
if (msg.has_oscillating) if (msg.has_oscillating)
@ -504,11 +509,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::light_command(const LightCommandRequest &msg) { void APIConnection::light_command(const LightCommandRequest &msg) {
light::LightState *light = App.get_light_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
if (light == nullptr)
return;
auto call = light->make_call();
if (msg.has_state) if (msg.has_state)
call.set_state(msg.state); call.set_state(msg.state);
if (msg.has_brightness) if (msg.has_brightness)
@ -597,9 +598,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::switch_command(const SwitchCommandRequest &msg) { void APIConnection::switch_command(const SwitchCommandRequest &msg) {
switch_::Switch *a_switch = App.get_switch_by_key(msg.key); ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
if (a_switch == nullptr)
return;
if (msg.state) { if (msg.state) {
a_switch->turn_on(); a_switch->turn_on();
@ -708,11 +707,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::climate_command(const ClimateCommandRequest &msg) { void APIConnection::climate_command(const ClimateCommandRequest &msg) {
climate::Climate *climate = App.get_climate_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
if (climate == nullptr)
return;
auto call = climate->make_call();
if (msg.has_mode) if (msg.has_mode)
call.set_mode(static_cast<climate::ClimateMode>(msg.mode)); call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
if (msg.has_target_temperature) if (msg.has_target_temperature)
@ -767,11 +762,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::number_command(const NumberCommandRequest &msg) { void APIConnection::number_command(const NumberCommandRequest &msg) {
number::Number *number = App.get_number_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
if (number == nullptr)
return;
auto call = number->make_call();
call.set_value(msg.state); call.set_value(msg.state);
call.perform(); call.perform();
} }
@ -801,11 +792,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::date_command(const DateCommandRequest &msg) { void APIConnection::date_command(const DateCommandRequest &msg) {
datetime::DateEntity *date = App.get_date_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
if (date == nullptr)
return;
auto call = date->make_call();
call.set_date(msg.year, msg.month, msg.day); call.set_date(msg.year, msg.month, msg.day);
call.perform(); call.perform();
} }
@ -835,11 +822,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::time_command(const TimeCommandRequest &msg) { void APIConnection::time_command(const TimeCommandRequest &msg) {
datetime::TimeEntity *time = App.get_time_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
if (time == nullptr)
return;
auto call = time->make_call();
call.set_time(msg.hour, msg.minute, msg.second); call.set_time(msg.hour, msg.minute, msg.second);
call.perform(); call.perform();
} }
@ -871,11 +854,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
if (datetime == nullptr)
return;
auto call = datetime->make_call();
call.set_datetime(msg.epoch_seconds); call.set_datetime(msg.epoch_seconds);
call.perform(); call.perform();
} }
@ -909,11 +888,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::text_command(const TextCommandRequest &msg) { void APIConnection::text_command(const TextCommandRequest &msg) {
text::Text *text = App.get_text_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
if (text == nullptr)
return;
auto call = text->make_call();
call.set_value(msg.state); call.set_value(msg.state);
call.perform(); call.perform();
} }
@ -945,11 +920,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::select_command(const SelectCommandRequest &msg) { void APIConnection::select_command(const SelectCommandRequest &msg) {
select::Select *select = App.get_select_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
if (select == nullptr)
return;
auto call = select->make_call();
call.set_option(msg.state); call.set_option(msg.state);
call.perform(); call.perform();
} }
@ -966,10 +937,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
button::Button *button = App.get_button_by_key(msg.key); ENTITY_COMMAND_GET(button::Button, button, button)
if (button == nullptr)
return;
button->press(); button->press();
} }
#endif #endif
@ -1000,9 +968,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::lock_command(const LockCommandRequest &msg) { void APIConnection::lock_command(const LockCommandRequest &msg) {
lock::Lock *a_lock = App.get_lock_by_key(msg.key); ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
if (a_lock == nullptr)
return;
switch (msg.command) { switch (msg.command) {
case enums::LOCK_UNLOCK: case enums::LOCK_UNLOCK:
@ -1045,11 +1011,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::valve_command(const ValveCommandRequest &msg) { void APIConnection::valve_command(const ValveCommandRequest &msg) {
valve::Valve *valve = App.get_valve_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
if (valve == nullptr)
return;
auto call = valve->make_call();
if (msg.has_position) if (msg.has_position)
call.set_position(msg.position); call.set_position(msg.position);
if (msg.stop) if (msg.stop)
@ -1096,11 +1058,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
if (media_player == nullptr)
return;
auto call = media_player->make_call();
if (msg.has_command) { if (msg.has_command) {
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command)); call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
} }
@ -1218,66 +1176,53 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
bool APIConnection::check_voice_assistant_api_connection_() const {
return voice_assistant::global_voice_assistant != nullptr &&
voice_assistant::global_voice_assistant->get_api_connection() == this;
}
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
} }
} }
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (!this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) { return;
return; }
}
if (msg.error) { if (msg.error) {
voice_assistant::global_voice_assistant->failed_to_start(); voice_assistant::global_voice_assistant->failed_to_start();
return; return;
} }
if (msg.port == 0) { if (msg.port == 0) {
// Use API Audio // Use API Audio
voice_assistant::global_voice_assistant->start_streaming(); voice_assistant::global_voice_assistant->start_streaming();
} else { } else {
struct sockaddr_storage storage; struct sockaddr_storage storage;
socklen_t len = sizeof(storage); socklen_t len = sizeof(storage);
this->helper_->getpeername((struct sockaddr *) &storage, &len); this->helper_->getpeername((struct sockaddr *) &storage, &len);
voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
}
} }
}; };
void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_event(msg); voice_assistant::global_voice_assistant->on_event(msg);
} }
} }
void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) { void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_audio(msg); voice_assistant::global_voice_assistant->on_audio(msg);
} }
}; };
void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) { void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_timer_event(msg); voice_assistant::global_voice_assistant->on_timer_event(msg);
} }
}; };
void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) { void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_announce(msg); voice_assistant::global_voice_assistant->on_announce(msg);
} }
} }
@ -1285,35 +1230,29 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) { const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp; VoiceAssistantConfigurationResponse resp;
if (voice_assistant::global_voice_assistant != nullptr) { if (!this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) { return resp;
return resp;
}
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
} }
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
return resp; return resp;
} }
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (this->check_voice_assistant_api_connection_()) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
} }
} }
@ -1346,11 +1285,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
is_single); is_single);
} }
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
if (a_alarm_control_panel == nullptr)
return;
auto call = a_alarm_control_panel->make_call();
switch (msg.command) { switch (msg.command) {
case enums::ALARM_CONTROL_PANEL_DISARM: case enums::ALARM_CONTROL_PANEL_DISARM:
call.disarm(); call.disarm();
@ -1438,9 +1373,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::update_command(const UpdateCommandRequest &msg) { void APIConnection::update_command(const UpdateCommandRequest &msg) {
update::UpdateEntity *update = App.get_update_by_key(msg.key); ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
if (update == nullptr)
return;
switch (msg.command) { switch (msg.command) {
case enums::UPDATE_COMMAND_UPDATE: case enums::UPDATE_COMMAND_UPDATE:
@ -1459,12 +1392,11 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
} }
#endif #endif
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) { bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
if (this->flags_.log_subscription < level) if (this->flags_.log_subscription < level)
return false; return false;
// Pre-calculate message size to avoid reallocations // Pre-calculate message size to avoid reallocations
const size_t line_length = strlen(line);
uint32_t msg_size = 0; uint32_t msg_size = 0;
// Add size for level field (field ID 1, varint type) // Add size for level field (field ID 1, varint type)
@ -1473,14 +1405,14 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
// Add size for string field (field ID 3, string type) // Add size for string field (field ID 3, string type)
// 1 byte for field tag + size of length varint + string length // 1 byte for field tag + size of length varint + string length
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(line_length)) + line_length; msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(message_len)) + message_len;
// Create a pre-sized buffer // Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size); auto buffer = this->create_buffer(msg_size);
// Encode the message (SubscribeLogsResponse) // Encode the message (SubscribeLogsResponse)
buffer.encode_uint32(1, static_cast<uint32_t>(level)); // LogLevel level = 1 buffer.encode_uint32(1, static_cast<uint32_t>(level)); // LogLevel level = 1
buffer.encode_string(3, line, line_length); // string message = 3 buffer.encode_string(3, line, message_len); // string message = 3
// SubscribeLogsResponse - 29 // SubscribeLogsResponse - 29
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);

View File

@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection {
bool send_media_player_state(media_player::MediaPlayer *media_player); bool send_media_player_state(media_player::MediaPlayer *media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override; void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif #endif
bool try_send_log_message(int level, const char *tag, const char *line); bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->flags_.service_call_subscription) if (!this->flags_.service_call_subscription)
return; return;
@ -301,6 +301,11 @@ class APIConnection : public APIServerConnection {
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
#ifdef USE_VOICE_ASSISTANT
// Helper to check voice assistant validity and connection ownership
inline bool check_voice_assistant_api_connection_() const;
#endif
// Helper method to process multiple entities from an iterator in a batch // Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) { template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size(); size_t initial_size = this->deferred_batch_.size();

View File

@ -225,6 +225,22 @@ APIError APIFrameHelper::init_common_() {
} }
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__) #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
return APIError::OK;
}
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
@ -327,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// no header information yet // no header information yet
uint8_t to_read = 3 - rx_header_buf_len_; uint8_t to_read = 3 - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (received == -1) { APIError err = handle_socket_read_result_(received);
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (err != APIError::OK) {
return APIError::WOULD_BLOCK; return err;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
} }
rx_header_buf_len_ += static_cast<uint8_t>(received); rx_header_buf_len_ += static_cast<uint8_t>(received);
if (static_cast<uint8_t>(received) != to_read) { if (static_cast<uint8_t>(received) != to_read) {
@ -372,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// more data to read // more data to read
uint16_t to_read = msg_size - rx_buf_len_; uint16_t to_read = msg_size - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) { APIError err = handle_socket_read_result_(received);
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (err != APIError::OK) {
return APIError::WOULD_BLOCK; return err;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
} }
rx_buf_len_ += static_cast<uint16_t>(received); rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) { if (static_cast<uint16_t>(received) != to_read) {
@ -855,17 +855,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
ssize_t received = ssize_t received =
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
if (received == -1) { APIError err = handle_socket_read_result_(received);
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (err != APIError::OK) {
return APIError::WOULD_BLOCK; return err;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
} }
// If this was the first read, validate the indicator byte // If this was the first read, validate the indicator byte
@ -949,17 +941,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// more data to read // more data to read
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) { APIError err = handle_socket_read_result_(received);
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (err != APIError::OK) {
return APIError::WOULD_BLOCK; return err;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
} }
rx_buf_len_ += static_cast<uint16_t>(received); rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) { if (static_cast<uint16_t>(received) != to_read) {

View File

@ -176,6 +176,9 @@ class APIFrameHelper {
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();
// Helper method to handle socket read results
APIError handle_socket_read_result_(ssize_t received);
}; };
#ifdef USE_API_NOISE #ifdef USE_API_NOISE

File diff suppressed because it is too large Load Diff

View File

@ -104,18 +104,19 @@ void APIServer::setup() {
#ifdef USE_LOGGER #ifdef USE_LOGGER
if (logger::global_logger != nullptr) { if (logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { logger::global_logger->add_on_log_callback(
if (this->shutting_down_) { [this](int level, const char *tag, const char *message, size_t message_len) {
// Don't try to send logs during shutdown if (this->shutting_down_) {
// as it could result in a recursion and // Don't try to send logs during shutdown
// we would be filling a buffer we are trying to clear // as it could result in a recursion and
return; // we would be filling a buffer we are trying to clear
} return;
for (auto &c : this->clients_) { }
if (!c->flags_.remove) for (auto &c : this->clients_) {
c->try_send_log_message(level, tag, message); if (!c->flags_.remove)
} c->try_send_log_message(level, tag, message, message_len);
}); }
});
} }
#endif #endif
@ -260,180 +261,114 @@ bool APIServer::check_password(const std::string &password) const {
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_binary_sensor_state(obj);
}
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
void APIServer::on_cover_update(cover::Cover *obj) { API_DISPATCH_UPDATE(cover::Cover, cover)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_cover_state(obj);
}
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
void APIServer::on_fan_update(fan::Fan *obj) { API_DISPATCH_UPDATE(fan::Fan, fan)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_fan_state(obj);
}
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
void APIServer::on_light_update(light::LightState *obj) { API_DISPATCH_UPDATE(light::LightState, light)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_light_state(obj);
}
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_sensor_state(obj);
}
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void APIServer::on_switch_update(switch_::Switch *obj, bool state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_switch_state(obj);
}
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_text_sensor_state(obj);
}
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void APIServer::on_climate_update(climate::Climate *obj) { API_DISPATCH_UPDATE(climate::Climate, climate)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_climate_state(obj);
}
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void APIServer::on_number_update(number::Number *obj, float state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_number_state(obj);
}
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void APIServer::on_date_update(datetime::DateEntity *obj) { API_DISPATCH_UPDATE(datetime::DateEntity, date)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_date_state(obj);
}
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
void APIServer::on_time_update(datetime::TimeEntity *obj) { API_DISPATCH_UPDATE(datetime::TimeEntity, time)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_time_state(obj);
}
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_datetime_state(obj);
}
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
void APIServer::on_text_update(text::Text *obj, const std::string &state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_text_state(obj);
}
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_select_state(obj);
}
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void APIServer::on_lock_update(lock::Lock *obj) { API_DISPATCH_UPDATE(lock::Lock, lock)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_lock_state(obj);
}
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
void APIServer::on_valve_update(valve::Valve *obj) { API_DISPATCH_UPDATE(valve::Valve, valve)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_valve_state(obj);
}
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_media_player_state(obj);
}
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - it's the only entity that passes extra parameters to the send method
void APIServer::on_event(event::Event *obj, const std::string &event_type) { void APIServer::on_event(event::Event *obj, const std::string &event_type) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_event(obj, event_type); c->send_event(obj, event_type);
} }
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
// Update is a special case - the method is called on_update, not on_update_update
void APIServer::on_update(update::UpdateEntity *obj) { void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_update_state(obj); c->send_update_state(obj);
} }
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_alarm_control_panel_state(obj);
}
#endif #endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }

View File

@ -52,11 +52,21 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
return true; return true;
} }
static constexpr size_t FLUSH_BATCH_SIZE = 8; // Batch size for BLE advertisements to maximize WiFi efficiency
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { // Each advertisement is up to 80 bytes when packaged (including protocol overhead)
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer; // Most advertisements are 20-30 bytes, allowing even more to fit per packet
return batch_buffer; // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
} // This achieves ~97% WiFi MTU utilization while staying under the limit
static constexpr size_t FLUSH_BATCH_SIZE = 16;
namespace {
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
// This is initialized at program startup before any threads
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
} // namespace
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)

View File

@ -2,6 +2,7 @@
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"

View File

@ -1,4 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BLOCK, CONF_BLOCK,
@ -7,6 +8,7 @@ from esphome.const import (
CONF_FREE, CONF_FREE,
CONF_ID, CONF_ID,
CONF_LOOP_TIME, CONF_LOOP_TIME,
PlatformFramework,
) )
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@OttoWinter"]
@ -44,3 +46,21 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"debug_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"debug_host.cpp": {PlatformFramework.HOST_NATIVE},
"debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"debug_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -1,6 +1,6 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import time from esphome.components import esp32, time
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
@ -11,6 +11,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEFAULT, CONF_DEFAULT,
@ -27,6 +28,7 @@ from esphome.const import (
CONF_WAKEUP_PIN, CONF_WAKEUP_PIN,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PlatformFramework,
) )
WAKEUP_PINS = { WAKEUP_PINS = {
@ -114,12 +116,20 @@ def validate_pin_number(value):
return value return value
def validate_config(config): def _validate_ex1_wakeup_mode(value):
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: if value == "ALL_LOW":
raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: if value == "ANY_LOW":
raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") esp32.only_on_variant(
return config supported=[
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
],
msg_prefix="ANY_LOW",
)(value)
return value
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
@ -146,6 +156,7 @@ WAKEUP_PIN_MODES = {
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
EXT1_WAKEUP_MODES = { EXT1_WAKEUP_MODES = {
"ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW,
"ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
"ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
} }
@ -185,16 +196,28 @@ CONFIG_SCHEMA = cv.All(
), ),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
),
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_PINS): cv.ensure_list( cv.Required(CONF_PINS): cv.ensure_list(
pins.internal_gpio_input_pin_schema, validate_pin_number pins.internal_gpio_input_pin_schema, validate_pin_number
), ),
cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True),
_validate_ex1_wakeup_mode,
),
} }
), ),
), ),
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
),
cv.boolean,
),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
@ -313,3 +336,14 @@ async def deep_sleep_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
return var return var
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"deep_sleep_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
}
)

View File

@ -189,7 +189,7 @@ def get_download_types(storage_json):
] ]
def only_on_variant(*, supported=None, unsupported=None): def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
"""Config validator for features only available on some ESP32 variants.""" """Config validator for features only available on some ESP32 variants."""
if supported is not None and not isinstance(supported, list): if supported is not None and not isinstance(supported, list):
supported = [supported] supported = [supported]
@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None):
variant = get_esp32_variant() variant = get_esp32_variant()
if supported is not None and variant not in supported: if supported is not None and variant not in supported:
raise cv.Invalid( raise cv.Invalid(
f"This feature is only available on {', '.join(supported)}" f"{msg_prefix} is only available on {', '.join(supported)}"
) )
if unsupported is not None and variant in unsupported: if unsupported is not None and variant in unsupported:
raise cv.Invalid( raise cv.Invalid(
f"This feature is not available on {', '.join(unsupported)}" f"{msg_prefix} is not available on {', '.join(unsupported)}"
) )
return obj return obj

View File

@ -0,0 +1,69 @@
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#include "esp_efuse.h"
#include "esp_efuse_table.h"
#include "esp_mac.h"
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
#include "esp_random.h"
#include "esp_system.h"
namespace esphome {
uint32_t random_uint32() { return esp_random(); }
bool random_bytes(uint8_t *data, size_t len) {
esp_fill_random(data, len);
return true;
}
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
Mutex::~Mutex() {}
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
// only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
// returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
if (has_custom_mac_address()) {
esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
} else {
esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
}
#else
if (has_custom_mac_address()) {
esp_efuse_mac_get_custom(mac);
} else {
esp_efuse_mac_get_default(mac);
}
#endif
}
void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
bool has_custom_mac_address() {
#if !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC)
uint8_t mac[6];
// do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails
#ifndef USE_ESP32_VARIANT_ESP32
return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
#else
return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
#endif
#else
return false;
#endif
}
} // namespace esphome
#endif // USE_ESP32

View File

@ -25,10 +25,15 @@ namespace esphome {
namespace esp32_ble { namespace esp32_ble {
// Maximum number of BLE scan results to buffer // Maximum number of BLE scan results to buffer
// Sized to handle bursts of advertisements while allowing for processing delays
// With 16 advertisements per batch and some safety margin:
// - Without PSRAM: 24 entries (1.5× batch size)
// - With PSRAM: 36 entries (2.25× batch size)
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
#ifdef USE_PSRAM #ifdef USE_PSRAM
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
#else #else
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
#endif #endif
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue // Maximum size of the BLE event queue - must be power of 2 for lock-free queue

View File

@ -0,0 +1,31 @@
#include "esphome/core/helpers.h"
#ifdef USE_ESP8266
#include <osapi.h>
#include <user_interface.h>
// for xt_rsil()/xt_wsr_ps()
#include <Arduino.h>
namespace esphome {
uint32_t random_uint32() { return os_random(); }
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
Mutex::Mutex() {}
Mutex::~Mutex() {}
void Mutex::lock() {}
bool Mutex::try_lock() { return true; }
void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
wifi_get_macaddr(STATION_IF, mac);
}
} // namespace esphome
#endif // USE_ESP8266

View File

@ -0,0 +1,68 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "gl_r01_i2c.h"
namespace esphome {
namespace gl_r01_i2c {
static const char *const TAG = "gl_r01_i2c";
// Register definitions from datasheet
static const uint8_t REG_VERSION = 0x00;
static const uint8_t REG_DISTANCE = 0x02;
static const uint8_t REG_TRIGGER = 0x10;
static const uint8_t CMD_TRIGGER = 0xB0;
static const uint8_t RESTART_CMD1 = 0x5A;
static const uint8_t RESTART_CMD2 = 0xA5;
static const uint8_t READ_DELAY = 40; // minimum milliseconds from datasheet to safely read measurement result
void GLR01I2CComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C...");
// Verify sensor presence
if (!this->read_byte_16(REG_VERSION, &this->version_)) {
ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!");
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_);
}
void GLR01I2CComponent::dump_config() {
ESP_LOGCONFIG(TAG, "GL-R01 I2C:");
ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_);
LOG_I2C_DEVICE(this);
LOG_SENSOR(" ", "Distance", this);
}
void GLR01I2CComponent::update() {
// Trigger a new measurement
if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) {
ESP_LOGE(TAG, "Failed to trigger measurement!");
this->status_set_warning();
return;
}
// Schedule reading the result after the read delay
this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); });
}
void GLR01I2CComponent::read_distance_() {
uint16_t distance = 0;
if (!this->read_byte_16(REG_DISTANCE, &distance)) {
ESP_LOGE(TAG, "Failed to read distance value!");
this->status_set_warning();
return;
}
if (distance == 0xFFFF) {
ESP_LOGW(TAG, "Invalid measurement received!");
this->status_set_warning();
} else {
ESP_LOGV(TAG, "Distance: %umm", distance);
this->publish_state(distance);
this->status_clear_warning();
}
}
} // namespace gl_r01_i2c
} // namespace esphome

View File

@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace gl_r01_i2c {
class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent {
public:
void setup() override;
void dump_config() override;
void update() override;
protected:
void read_distance_();
uint16_t version_{0};
};
} // namespace gl_r01_i2c
} // namespace esphome

View File

@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER,
)
CODEOWNERS = ["@pkejval"]
DEPENDENCIES = ["i2c"]
gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
GLR01I2CComponent = gl_r01_i2c_ns.class_(
"GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
GLR01I2CComponent,
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x74))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await i2c.register_i2c_device(var, config)

View File

@ -0,0 +1,57 @@
#include "esphome/core/helpers.h"
#ifdef USE_HOST
#ifndef _WIN32
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#endif
#include <unistd.h>
#include <limits>
#include <random>
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
namespace esphome {
static const char *const TAG = "helpers.host";
uint32_t random_uint32() {
std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max());
return dist(rng);
}
bool random_bytes(uint8_t *data, size_t len) {
FILE *fp = fopen("/dev/urandom", "r");
if (fp == nullptr) {
ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
exit(1);
}
size_t read = fread(data, 1, len, fp);
if (read != len) {
ESP_LOGW(TAG, "Not enough data from /dev/urandom");
exit(1);
}
fclose(fp);
return true;
}
// Host platform uses std::mutex for proper thread synchronization
Mutex::Mutex() { handle_ = new std::mutex(); }
Mutex::~Mutex() { delete static_cast<std::mutex *>(handle_); }
void Mutex::lock() { static_cast<std::mutex *>(handle_)->lock(); }
bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock(); }
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
}
} // namespace esphome
#endif // USE_HOST

View File

@ -2,6 +2,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32 from esphome.components import esp32
from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ESP8266_DISABLE_SSL_SUPPORT,
@ -13,6 +14,7 @@ from esphome.const import (
CONF_URL, CONF_URL,
CONF_WATCHDOG_TIMEOUT, CONF_WATCHDOG_TIMEOUT,
PLATFORM_HOST, PLATFORM_HOST,
PlatformFramework,
__version__, __version__,
) )
from esphome.core import CORE, Lambda from esphome.core import CORE, Lambda
@ -319,3 +321,19 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
return var return var
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"http_request_host.cpp": {PlatformFramework.HOST_NATIVE},
"http_request_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"http_request_idf.cpp": {PlatformFramework.ESP32_IDF},
}
)

View File

@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MOISTURE): sensor.sensor_schema( cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
unit_of_measurement=UNIT_INTENSITY, unit_of_measurement=UNIT_INTENSITY,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:weather-rainy",
), ),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,

View File

@ -3,6 +3,7 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32 from esphome.components import esp32
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ADDRESS, CONF_ADDRESS,
@ -18,6 +19,7 @@ from esphome.const import (
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
PlatformFramework,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
@ -205,3 +207,18 @@ def final_validate_device_schema(
{cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)}, {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)},
extra=cv.ALLOW_EXTRA, extra=cv.ALLOW_EXTRA,
) )
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"i2c_bus_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
}
)

View File

@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError
from esphome import core, external_files from esphome import core, external_files
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEFAULTS,
CONF_DITHER, CONF_DITHER,
CONF_FILE, CONF_FILE,
CONF_ICON, CONF_ICON,
@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque"
CONF_CHROMA_KEY = "chroma_key" CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel" CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha" CONF_INVERT_ALPHA = "invert_alpha"
CONF_IMAGES = "images"
TRANSPARENCY_TYPES = ( TRANSPARENCY_TYPES = (
CONF_OPAQUE, CONF_OPAQUE,
@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder):
dither, dither,
invert_alpha, invert_alpha,
) )
self.big_endian = True
def set_big_endian(self, big_endian: bool) -> None:
self.big_endian = big_endian
def convert(self, image, path): def convert(self, image, path):
return image.convert("RGBA") return image.convert("RGBA")
@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder):
g = 1 g = 1
b = 0 b = 0
rgb = (r << 11) | (g << 5) | b rgb = (r << 11) | (g << 5) | b
self.data[self.index] = rgb >> 8 if self.big_endian:
self.index += 1 self.data[self.index] = rgb >> 8
self.data[self.index] = rgb & 0xFF self.index += 1
self.index += 1 self.data[self.index] = rgb & 0xFF
self.index += 1
else:
self.data[self.index] = rgb & 0xFF
self.index += 1
self.data[self.index] = rgb >> 8
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL: if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha: if self.invert_alpha:
a ^= 0xFF a ^= 0xFF
@ -364,7 +377,7 @@ def validate_file_shorthand(value):
value = cv.string_strict(value) value = cv.string_strict(value)
parts = value.strip().split(":") parts = value.strip().split(":")
if len(parts) == 2 and parts[0] in MDI_SOURCES: if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"[a-zA-Z0-9\-]+", parts[1]) match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
if match is None: if match is None:
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.") raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
return download_gh_svg(parts[1], parts[0]) return download_gh_svg(parts[1], parts[0])
@ -434,20 +447,29 @@ def validate_type(image_types):
def validate_settings(value): def validate_settings(value):
type = value[CONF_TYPE] """
Validate the settings for a single image configuration.
"""
conf_type = value[CONF_TYPE]
type_class = IMAGE_TYPE[conf_type]
transparency = value[CONF_TRANSPARENCY].lower() transparency = value[CONF_TRANSPARENCY].lower()
allow_config = IMAGE_TYPE[type].allow_config if transparency not in type_class.allow_config:
if transparency not in allow_config:
raise cv.Invalid( raise cv.Invalid(
f"Image format '{type}' cannot have transparency: {transparency}" f"Image format '{conf_type}' cannot have transparency: {transparency}"
) )
invert_alpha = value.get(CONF_INVERT_ALPHA, False) invert_alpha = value.get(CONF_INVERT_ALPHA, False)
if ( if (
invert_alpha invert_alpha
and transparency != CONF_ALPHA_CHANNEL and transparency != CONF_ALPHA_CHANNEL
and CONF_INVERT_ALPHA not in allow_config and CONF_INVERT_ALPHA not in type_class.allow_config
): ):
raise cv.Invalid("No alpha channel to invert") raise cv.Invalid("No alpha channel to invert")
if value.get(CONF_BYTE_ORDER) is not None and not callable(
getattr(type_class, "set_big_endian", None)
):
raise cv.Invalid(
f"Image format '{conf_type}' does not support byte order configuration"
)
if file := value.get(CONF_FILE): if file := value.get(CONF_FILE):
file = Path(file) file = Path(file)
if is_svg_file(file): if is_svg_file(file):
@ -456,31 +478,82 @@ def validate_settings(value):
try: try:
Image.open(file) Image.open(file)
except UnidentifiedImageError as exc: except UnidentifiedImageError as exc:
raise cv.Invalid(f"File can't be opened as image: {file}") from exc raise cv.Invalid(
f"File can't be opened as image: {file.absolute()}"
) from exc
return value return value
IMAGE_ID_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
OPTIONS_SCHEMA = {
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
}
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
# image schema with no defaults, used with `CONF_IMAGES` in the config
IMAGE_SCHEMA_NO_DEFAULTS = {
**IMAGE_ID_SCHEMA,
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
}
BASE_SCHEMA = cv.Schema( BASE_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ID): cv.declare_id(Image_), **IMAGE_ID_SCHEMA,
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA), **OPTIONS_SCHEMA,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
} }
).add_extra(validate_settings) ).add_extra(validate_settings)
IMAGE_SCHEMA = BASE_SCHEMA.extend( IMAGE_SCHEMA = BASE_SCHEMA.extend(
{ {
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
} }
) )
def validate_defaults(value):
"""
Validate the options for images with defaults
"""
defaults = value[CONF_DEFAULTS]
result = []
for index, image in enumerate(value[CONF_IMAGES]):
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
if type is None:
raise cv.Invalid(
"Type is required either in the image config or in the defaults",
path=[CONF_IMAGES, index],
)
type_class = IMAGE_TYPE[type]
# A default byte order should be simply ignored if the type does not support it
available_options = [*OPTIONS]
if (
not callable(getattr(type_class, "set_big_endian", None))
and CONF_BYTE_ORDER not in image
):
available_options.remove(CONF_BYTE_ORDER)
config = {
**{key: image.get(key, defaults.get(key)) for key in available_options},
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
}
validate_settings(config)
result.append(config)
return result
def typed_image_schema(image_type): def typed_image_schema(image_type):
""" """
Construct a schema for a specific image type, allowing transparency options Construct a schema for a specific image type, allowing transparency options
@ -523,10 +596,33 @@ def typed_image_schema(image_type):
# The config schema can be a (possibly empty) single list of images, # The config schema can be a (possibly empty) single list of images,
# or a dictionary of image types each with a list of images # or a dictionary of image types each with a list of images
CONFIG_SCHEMA = cv.Any( # or a dictionary with keys `defaults:` and `images:`
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
cv.ensure_list(IMAGE_SCHEMA),
) def _config_schema(config):
if isinstance(config, list):
return cv.Schema([IMAGE_SCHEMA])(config)
if not isinstance(config, dict):
raise cv.Invalid(
"Badly formed image configuration, expected a list or a dictionary"
)
if CONF_DEFAULTS in config or CONF_IMAGES in config:
return validate_defaults(
cv.Schema(
{
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
}
)(config)
)
if CONF_ID in config or CONF_FILE in config:
return cv.ensure_list(IMAGE_SCHEMA)([config])
return cv.Schema(
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
)(config)
CONFIG_SCHEMA = _config_schema
async def write_image(config, all_frames=False): async def write_image(config, all_frames=False):
@ -585,6 +681,9 @@ async def write_image(config, all_frames=False):
total_rows = height * frame_count total_rows = height * frame_count
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
if byte_order := config.get(CONF_BYTE_ORDER):
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
for frame_index in range(frame_count): for frame_index in range(frame_count):
image.seek(frame_index) image.seek(frame_index)
pixels = encoder.convert(image.resize((width, height)), path).getdata() pixels = encoder.convert(image.resize((width, height)), path).getdata()

View File

@ -1,6 +1,7 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, i2c from esphome.components import display, i2c
from esphome.components.esp32 import CONF_CPU_FREQUENCY
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_FULL_UPDATE_EVERY, CONF_FULL_UPDATE_EVERY,
@ -13,7 +14,9 @@ from esphome.const import (
CONF_PAGES, CONF_PAGES,
CONF_TRANSFORM, CONF_TRANSFORM,
CONF_WAKEUP_PIN, CONF_WAKEUP_PIN,
PLATFORM_ESP32,
) )
import esphome.final_validate as fv
DEPENDENCIES = ["i2c", "esp32"] DEPENDENCIES = ["i2c", "esp32"]
AUTO_LOAD = ["psram"] AUTO_LOAD = ["psram"]
@ -120,6 +123,18 @@ CONFIG_SCHEMA = cv.All(
) )
def _validate_cpu_frequency(config):
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
raise cv.Invalid(
"Inkplate requires 240MHz CPU frequency (set in esp32 component)"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])

View File

@ -13,13 +13,13 @@ from esphome.const import (
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
ResetButton = ld2450_ns.class_("ResetButton", button.Button) FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button)
RestartButton = ld2450_ns.class_("RestartButton", button.Button) RestartButton = ld2450_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema( cv.Optional(CONF_FACTORY_RESET): button.button_schema(
ResetButton, FactoryResetButton,
device_class=DEVICE_CLASS_RESTART, device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG, entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT, icon=ICON_RESTART_ALERT,
@ -38,7 +38,7 @@ async def to_code(config):
if factory_reset_config := config.get(CONF_FACTORY_RESET): if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config) b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2450_ID]) await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_reset_button(b)) cg.add(ld2450_component.set_factory_reset_button(b))
if restart_config := config.get(CONF_RESTART): if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config) b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2450_ID]) await cg.register_parented(b, config[CONF_LD2450_ID])

View File

@ -0,0 +1,9 @@
#include "factory_reset_button.h"
namespace esphome {
namespace ld2450 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace ld2450
} // namespace esphome

View File

@ -6,9 +6,9 @@
namespace esphome { namespace esphome {
namespace ld2450 { namespace ld2450 {
class ResetButton : public button::Button, public Parented<LD2450Component> { class FactoryResetButton : public button::Button, public Parented<LD2450Component> {
public: public:
ResetButton() = default; FactoryResetButton() = default;
protected: protected:
void press_action() override; void press_action() override;

View File

@ -1,9 +0,0 @@
#include "reset_button.h"
namespace esphome {
namespace ld2450 {
void ResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace ld2450
} // namespace esphome

View File

@ -18,11 +18,10 @@ namespace esphome {
namespace ld2450 { namespace ld2450 {
static const char *const TAG = "ld2450"; static const char *const TAG = "ld2450";
static const char *const NO_MAC = "08:05:04:03:02:01";
static const char *const UNKNOWN_MAC = "unknown"; static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
enum BaudRateStructure : uint8_t { enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1, BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2, BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3, BAUD_RATE_38400 = 3,
@ -33,14 +32,13 @@ enum BaudRateStructure : uint8_t {
BAUD_RATE_460800 = 8 BAUD_RATE_460800 = 8
}; };
// Zone type struct enum ZoneType : uint8_t {
enum ZoneTypeStructure : uint8_t {
ZONE_DISABLED = 0, ZONE_DISABLED = 0,
ZONE_DETECTION = 1, ZONE_DETECTION = 1,
ZONE_FILTER = 2, ZONE_FILTER = 2,
}; };
enum PeriodicDataStructure : uint8_t { enum PeriodicData : uint8_t {
TARGET_X = 4, TARGET_X = 4,
TARGET_Y = 6, TARGET_Y = 6,
TARGET_SPEED = 8, TARGET_SPEED = 8,
@ -48,12 +46,12 @@ enum PeriodicDataStructure : uint8_t {
}; };
enum PeriodicDataValue : uint8_t { enum PeriodicDataValue : uint8_t {
HEAD = 0xAA, HEADER = 0xAA,
END = 0x55, FOOTER = 0x55,
CHECK = 0x00, CHECK = 0x00,
}; };
enum AckDataStructure : uint8_t { enum AckData : uint8_t {
COMMAND = 6, COMMAND = 6,
COMMAND_STATUS = 7, COMMAND_STATUS = 7,
}; };
@ -61,11 +59,11 @@ enum AckDataStructure : uint8_t {
// Memory-efficient lookup tables // Memory-efficient lookup tables
struct StringToUint8 { struct StringToUint8 {
const char *str; const char *str;
uint8_t value; const uint8_t value;
}; };
struct Uint8ToString { struct Uint8ToString {
uint8_t value; const uint8_t value;
const char *str; const char *str;
}; };
@ -75,6 +73,13 @@ constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
}; };
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
{DIRECTION_APPROACHING, "Approaching"},
{DIRECTION_MOVING_AWAY, "Moving away"},
{DIRECTION_STATIONARY, "Stationary"},
{DIRECTION_NA, "NA"},
};
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
{ZONE_DISABLED, "Disabled"}, {ZONE_DISABLED, "Disabled"},
{ZONE_DETECTION, "Detection"}, {ZONE_DETECTION, "Detection"},
@ -104,28 +109,35 @@ template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t v
return ""; // Not found return ""; // Not found
} }
// LD2450 serial command header & footer
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
// LD2450 UART Serial Commands // LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0xFF; static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
static const uint8_t CMD_DISABLE_CONF = 0xFE; static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
static const uint8_t CMD_VERSION = 0xA0; static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
static const uint8_t CMD_MAC = 0xA5; static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
static const uint8_t CMD_RESET = 0xA2; static constexpr uint8_t CMD_RESET = 0xA2;
static const uint8_t CMD_RESTART = 0xA3; static constexpr uint8_t CMD_RESTART = 0xA3;
static const uint8_t CMD_BLUETOOTH = 0xA4; static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90;
static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91;
static const uint8_t CMD_SET_BAUD_RATE = 0xA1; static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
static const uint8_t CMD_QUERY_ZONE = 0xC1; static constexpr uint8_t CMD_QUERY_ZONE = 0xC1;
static const uint8_t CMD_SET_ZONE = 0xC2; static constexpr uint8_t CMD_SET_ZONE = 0xC2;
// Header & Footer size
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
// Command Header & Footer
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
// Data Header & Footer
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00};
static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
for (int i = 0; i < 4; i++) { for (uint8_t i = 0; i < 4; i++) {
uint16_t val = values[i] & 0xFFFF; uint16_t val = values[i] & 0xFFFF;
bytes[i * 2] = val & 0xFF; // Store low byte first (little-endian) bytes[i * 2] = val & 0xFF; // Store low byte first (little-endian)
bytes[i * 2 + 1] = (val >> 8) & 0xFF; // Store high byte second bytes[i * 2 + 1] = (val >> 8) & 0xFF; // Store high byte second
@ -166,18 +178,13 @@ static inline float calculate_angle(float base, float hypotenuse) {
return angle_degrees; return angle_degrees;
} }
static inline std::string get_direction(int16_t speed) { static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
static const char *const APPROACHING = "Approaching"; for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) {
static const char *const MOVING_AWAY = "Moving away"; if (header_footer[i] != buffer[i]) {
static const char *const STATIONARY = "Stationary"; return false; // Mismatch in header/footer
}
if (speed > 0) {
return MOVING_AWAY;
} }
if (speed < 0) { return true; // Valid header/footer
return APPROACHING;
}
return STATIONARY;
} }
void LD2450Component::setup() { void LD2450Component::setup() {
@ -192,84 +199,93 @@ void LD2450Component::setup() {
} }
void LD2450Component::dump_config() { void LD2450Component::dump_config() {
ESP_LOGCONFIG(TAG, "LD2450:"); std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGCONFIG(TAG,
"LD2450:\n"
" Firmware version: %s\n"
" MAC address: %s\n"
" Throttle: %u ms",
version.c_str(), mac_str.c_str(), this->throttle_);
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); ESP_LOGCONFIG(TAG, "Binary Sensors:");
LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_);
#endif LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
#ifdef USE_SWITCH
LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_);
LOG_SWITCH(" ", "MultiTargetSwitch", this->multi_target_switch_);
#endif
#ifdef USE_BUTTON
LOG_BUTTON(" ", "ResetButton", this->reset_button_);
LOG_BUTTON(" ", "RestartButton", this->restart_button_);
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_); ESP_LOGCONFIG(TAG, "Sensors:");
LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_); LOG_SENSOR(" ", "MovingTargetCount", this->moving_target_count_sensor_);
LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_); LOG_SENSOR(" ", "StillTargetCount", this->still_target_count_sensor_);
LOG_SENSOR(" ", "TargetCount", this->target_count_sensor_);
for (sensor::Sensor *s : this->move_x_sensors_) { for (sensor::Sensor *s : this->move_x_sensors_) {
LOG_SENSOR(" ", "NthTargetXSensor", s); LOG_SENSOR(" ", "TargetX", s);
} }
for (sensor::Sensor *s : this->move_y_sensors_) { for (sensor::Sensor *s : this->move_y_sensors_) {
LOG_SENSOR(" ", "NthTargetYSensor", s); LOG_SENSOR(" ", "TargetY", s);
}
for (sensor::Sensor *s : this->move_speed_sensors_) {
LOG_SENSOR(" ", "NthTargetSpeedSensor", s);
} }
for (sensor::Sensor *s : this->move_angle_sensors_) { for (sensor::Sensor *s : this->move_angle_sensors_) {
LOG_SENSOR(" ", "NthTargetAngleSensor", s); LOG_SENSOR(" ", "TargetAngle", s);
} }
for (sensor::Sensor *s : this->move_distance_sensors_) { for (sensor::Sensor *s : this->move_distance_sensors_) {
LOG_SENSOR(" ", "NthTargetDistanceSensor", s); LOG_SENSOR(" ", "TargetDistance", s);
} }
for (sensor::Sensor *s : this->move_resolution_sensors_) { for (sensor::Sensor *s : this->move_resolution_sensors_) {
LOG_SENSOR(" ", "NthTargetResolutionSensor", s); LOG_SENSOR(" ", "TargetResolution", s);
}
for (sensor::Sensor *s : this->move_speed_sensors_) {
LOG_SENSOR(" ", "TargetSpeed", s);
} }
for (sensor::Sensor *s : this->zone_target_count_sensors_) { for (sensor::Sensor *s : this->zone_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneTargetCountSensor", s); LOG_SENSOR(" ", "ZoneTargetCount", s);
}
for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s);
} }
for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneMovingTargetCountSensor", s); LOG_SENSOR(" ", "ZoneMovingTargetCount", s);
}
for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
LOG_SENSOR(" ", "ZoneStillTargetCount", s);
} }
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); ESP_LOGCONFIG(TAG, "Text Sensors:");
LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
LOG_TEXT_SENSOR(" ", "Mac", this->mac_text_sensor_);
for (text_sensor::TextSensor *s : this->direction_text_sensors_) { for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s); LOG_TEXT_SENSOR(" ", "Direction", s);
} }
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
ESP_LOGCONFIG(TAG, "Numbers:");
LOG_NUMBER(" ", "PresenceTimeout", this->presence_timeout_number_);
for (auto n : this->zone_numbers_) { for (auto n : this->zone_numbers_) {
LOG_NUMBER(" ", "ZoneX1Number", n.x1); LOG_NUMBER(" ", "ZoneX1", n.x1);
LOG_NUMBER(" ", "ZoneY1Number", n.y1); LOG_NUMBER(" ", "ZoneY1", n.y1);
LOG_NUMBER(" ", "ZoneX2Number", n.x2); LOG_NUMBER(" ", "ZoneX2", n.x2);
LOG_NUMBER(" ", "ZoneY2Number", n.y2); LOG_NUMBER(" ", "ZoneY2", n.y2);
} }
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); ESP_LOGCONFIG(TAG, "Selects:");
LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_); LOG_SELECT(" ", "BaudRate", this->baud_rate_select_);
LOG_SELECT(" ", "ZoneType", this->zone_type_select_);
#endif #endif
#ifdef USE_NUMBER #ifdef USE_SWITCH
LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_); ESP_LOGCONFIG(TAG, "Switches:");
LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_);
LOG_SWITCH(" ", "MultiTarget", this->multi_target_switch_);
#endif
#ifdef USE_BUTTON
ESP_LOGCONFIG(TAG, "Buttons:");
LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
LOG_BUTTON(" ", "Restart", this->restart_button_);
#endif #endif
ESP_LOGCONFIG(TAG,
" Throttle: %ums\n"
" MAC Address: %s\n"
" Firmware version: %s",
this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
} }
void LD2450Component::loop() { void LD2450Component::loop() {
while (this->available()) { while (this->available()) {
this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH); this->readline_(this->read());
} }
} }
@ -304,7 +320,7 @@ void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_
this->zone_type_ = zone_type; this->zone_type_ = zone_type;
int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1, int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2}; zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
for (int i = 0; i < MAX_ZONES; i++) { for (uint8_t i = 0; i < MAX_ZONES; i++) {
this->zone_config_[i].x1 = zone_parameters[i * 4]; this->zone_config_[i].x1 = zone_parameters[i * 4];
this->zone_config_[i].y1 = zone_parameters[i * 4 + 1]; this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
this->zone_config_[i].x2 = zone_parameters[i * 4 + 2]; this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
@ -318,15 +334,15 @@ void LD2450Component::send_set_zone_command_() {
uint8_t cmd_value[26] = {}; uint8_t cmd_value[26] = {};
uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00}; uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
uint8_t area_config[24] = {}; uint8_t area_config[24] = {};
for (int i = 0; i < MAX_ZONES; i++) { for (uint8_t i = 0; i < MAX_ZONES; i++) {
int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2, int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
this->zone_config_[i].y2}; this->zone_config_[i].y2};
ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
} }
std::memcpy(cmd_value, zone_type_bytes, 2); std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
std::memcpy(cmd_value + 2, area_config, 24); std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
this->set_config_mode_(true); this->set_config_mode_(true);
this->send_command_(CMD_SET_ZONE, cmd_value, 26); this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
this->set_config_mode_(false); this->set_config_mode_(false);
} }
@ -342,14 +358,14 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
} }
// Extract, store and publish zone details LD2450 buffer // Extract, store and publish zone details LD2450 buffer
void LD2450Component::process_zone_(uint8_t *buffer) { void LD2450Component::process_zone_() {
uint8_t index, start; uint8_t index, start;
for (index = 0; index < MAX_ZONES; index++) { for (index = 0; index < MAX_ZONES; index++) {
start = 12 + index * 8; start = 12 + index * 8;
this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start); this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2); this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4); this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6); this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
#ifdef USE_NUMBER #ifdef USE_NUMBER
// only one null check as all coordinates are required for a single zone // only one null check as all coordinates are required for a single zone
if (this->zone_numbers_[index].x1 != nullptr) { if (this->zone_numbers_[index].x1 != nullptr) {
@ -395,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() {
// Send command with values to LD2450 // Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending command %02X", command); ESP_LOGV(TAG, "Sending COMMAND %02X", command);
// frame header // frame header bytes
this->write_array(CMD_FRAME_HEADER, 4); this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
// length bytes // length bytes
int len = 2; uint8_t len = 2;
if (command_value != nullptr) { if (command_value != nullptr) {
len += command_value_len; len += command_value_len;
} }
this->write_byte(lowbyte(len)); uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00};
this->write_byte(highbyte(len)); this->write_array(len_cmd, sizeof(len_cmd));
// command
this->write_byte(lowbyte(command));
this->write_byte(highbyte(command));
// command value bytes // command value bytes
if (command_value != nullptr) { if (command_value != nullptr) {
for (int i = 0; i < command_value_len; i++) { for (uint8_t i = 0; i < command_value_len; i++) {
this->write_byte(command_value[i]); this->write_byte(command_value[i]);
} }
} }
// footer // frame footer bytes
this->write_array(CMD_FRAME_END, 4); this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
// FIXME to remove // FIXME to remove
delay(50); // NOLINT delay(50); // NOLINT
} }
@ -423,26 +437,23 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
// LD2450 Radar data message: // LD2450 Radar data message:
// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] // [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
// Header Target 1 Target 2 Target 3 End // Header Target 1 Target 2 Target 3 End
void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { void LD2450Component::handle_periodic_data_() {
// Early throttle check - moved before any processing to save CPU cycles // Early throttle check - moved before any processing to save CPU cycles
if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
return; return;
} }
if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Invalid message length"); ESP_LOGE(TAG, "Invalid length");
return; return;
} }
if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
ESP_LOGE(TAG, "Invalid message header"); this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
ESP_LOGE(TAG, "Invalid header/footer");
return; return;
} }
if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
ESP_LOGE(TAG, "Invalid message footer");
return;
}
this->last_periodic_millis_ = App.get_loop_component_start_time(); this->last_periodic_millis_ = App.get_loop_component_start_time();
int16_t target_count = 0; int16_t target_count = 0;
@ -450,13 +461,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
int16_t moving_target_count = 0; int16_t moving_target_count = 0;
int16_t start = 0; int16_t start = 0;
int16_t val = 0; int16_t val = 0;
uint8_t index = 0;
int16_t tx = 0; int16_t tx = 0;
int16_t ty = 0; int16_t ty = 0;
int16_t td = 0; int16_t td = 0;
int16_t ts = 0; int16_t ts = 0;
int16_t angle = 0; int16_t angle = 0;
std::string direction{}; uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false; bool is_moving = false;
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR) #if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
@ -468,7 +479,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
is_moving = false; is_moving = false;
sensor::Sensor *sx = this->move_x_sensors_[index]; sensor::Sensor *sx = this->move_x_sensors_[index];
if (sx != nullptr) { if (sx != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
tx = val; tx = val;
if (this->cached_target_data_[index].x != val) { if (this->cached_target_data_[index].x != val) {
sx->publish_state(val); sx->publish_state(val);
@ -479,7 +490,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
start = TARGET_Y + index * 8; start = TARGET_Y + index * 8;
sensor::Sensor *sy = this->move_y_sensors_[index]; sensor::Sensor *sy = this->move_y_sensors_[index];
if (sy != nullptr) { if (sy != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
ty = val; ty = val;
if (this->cached_target_data_[index].y != val) { if (this->cached_target_data_[index].y != val) {
sy->publish_state(val); sy->publish_state(val);
@ -490,7 +501,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
start = TARGET_RESOLUTION + index * 8; start = TARGET_RESOLUTION + index * 8;
sensor::Sensor *sr = this->move_resolution_sensors_[index]; sensor::Sensor *sr = this->move_resolution_sensors_[index];
if (sr != nullptr) { if (sr != nullptr) {
val = (buffer[start + 1] << 8) | buffer[start]; val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
if (this->cached_target_data_[index].resolution != val) { if (this->cached_target_data_[index].resolution != val) {
sr->publish_state(val); sr->publish_state(val);
this->cached_target_data_[index].resolution = val; this->cached_target_data_[index].resolution = val;
@ -499,7 +510,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
// SPEED // SPEED
start = TARGET_SPEED + index * 8; start = TARGET_SPEED + index * 8;
val = ld2450::decode_speed(buffer[start], buffer[start + 1]); val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
ts = val; ts = val;
if (val) { if (val) {
is_moving = true; is_moving = true;
@ -532,7 +543,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
} }
} }
// ANGLE // ANGLE
angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td)); angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) { if (tx > 0) {
angle = angle * -1; angle = angle * -1;
} }
@ -547,14 +558,19 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
// DIRECTION // DIRECTION
direction = get_direction(ts);
if (td == 0) { if (td == 0) {
direction = "NA"; direction = DIRECTION_NA;
} else if (ts > 0) {
direction = DIRECTION_MOVING_AWAY;
} else if (ts < 0) {
direction = DIRECTION_APPROACHING;
} else {
direction = DIRECTION_STATIONARY;
} }
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) { if (tsd != nullptr) {
if (this->cached_target_data_[index].direction != direction) { if (this->cached_target_data_[index].direction != direction) {
tsd->publish_state(direction); tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
this->cached_target_data_[index].direction = direction; this->cached_target_data_[index].direction = direction;
} }
} }
@ -678,117 +694,139 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
} }
bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { bool LD2450Component::handle_ack_data_() {
ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
if (len < 10) { if (this->buffer_pos_ < 10) {
ESP_LOGE(TAG, "Invalid ack length"); ESP_LOGE(TAG, "Invalid length");
return true; return true;
} }
if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]); ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
return true; return true;
} }
if (buffer[COMMAND_STATUS] != 0x01) { if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Invalid ack status"); ESP_LOGE(TAG, "Invalid status");
return true; return true;
} }
if (buffer[8] || buffer[9]) { if (this->buffer_data_[8] || this->buffer_data_[9]) {
ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]); ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
return true; return true;
} }
switch (buffer[COMMAND]) { switch (this->buffer_data_[COMMAND]) {
case lowbyte(CMD_ENABLE_CONF): case CMD_ENABLE_CONF:
ESP_LOGV(TAG, "Enable conf command"); ESP_LOGV(TAG, "Enable conf");
break; break;
case lowbyte(CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Disable conf command"); case CMD_DISABLE_CONF:
ESP_LOGV(TAG, "Disabled conf");
break; break;
case lowbyte(CMD_SET_BAUD_RATE):
ESP_LOGV(TAG, "Baud rate change command"); case CMD_SET_BAUD_RATE:
ESP_LOGV(TAG, "Baud rate change");
#ifdef USE_SELECT #ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) { if (this->baud_rate_select_ != nullptr) {
ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
} }
#endif #endif
break; break;
case lowbyte(CMD_VERSION):
this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); case CMD_QUERY_VERSION: {
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) { if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(this->version_); this->version_text_sensor_->publish_state(version);
} }
#endif #endif
break; break;
case lowbyte(CMD_MAC): }
if (len < 20) {
case CMD_QUERY_MAC_ADDRESS: {
if (this->buffer_pos_ < 20) {
return false; return false;
} }
this->mac_ = format_mac_address_pretty(&buffer[10]);
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
if (this->bluetooth_on_) {
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
}
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) { if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); this->mac_text_sensor_->publish_state(mac_str);
} }
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) { if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); this->bluetooth_switch_->publish_state(this->bluetooth_on_);
} }
#endif #endif
break; break;
case lowbyte(CMD_BLUETOOTH): }
ESP_LOGV(TAG, "Bluetooth command");
case CMD_BLUETOOTH:
ESP_LOGV(TAG, "Bluetooth");
break; break;
case lowbyte(CMD_SINGLE_TARGET_MODE):
ESP_LOGV(TAG, "Single target conf command"); case CMD_SINGLE_TARGET_MODE:
ESP_LOGV(TAG, "Single target conf");
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) { if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(false); this->multi_target_switch_->publish_state(false);
} }
#endif #endif
break; break;
case lowbyte(CMD_MULTI_TARGET_MODE):
ESP_LOGV(TAG, "Multi target conf command"); case CMD_MULTI_TARGET_MODE:
ESP_LOGV(TAG, "Multi target conf");
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) { if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(true); this->multi_target_switch_->publish_state(true);
} }
#endif #endif
break; break;
case lowbyte(CMD_QUERY_TARGET_MODE):
ESP_LOGV(TAG, "Query target tracking mode command"); case CMD_QUERY_TARGET_MODE:
ESP_LOGV(TAG, "Query target tracking mode");
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) { if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(buffer[10] == 0x02); this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
} }
#endif #endif
break; break;
case lowbyte(CMD_QUERY_ZONE):
ESP_LOGV(TAG, "Query zone conf command"); case CMD_QUERY_ZONE:
this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); ESP_LOGV(TAG, "Query zone conf");
this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16);
this->publish_zone_type(); this->publish_zone_type();
#ifdef USE_SELECT #ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) { if (this->zone_type_select_ != nullptr) {
ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
} }
#endif #endif
if (buffer[10] == 0x00) { if (this->buffer_data_[10] == 0x00) {
ESP_LOGV(TAG, "Zone: Disabled"); ESP_LOGV(TAG, "Zone: Disabled");
} }
if (buffer[10] == 0x01) { if (this->buffer_data_[10] == 0x01) {
ESP_LOGV(TAG, "Zone: Area detection"); ESP_LOGV(TAG, "Zone: Area detection");
} }
if (buffer[10] == 0x02) { if (this->buffer_data_[10] == 0x02) {
ESP_LOGV(TAG, "Zone: Area filter"); ESP_LOGV(TAG, "Zone: Area filter");
} }
this->process_zone_(buffer); this->process_zone_();
break; break;
case lowbyte(CMD_SET_ZONE):
ESP_LOGV(TAG, "Set zone conf command"); case CMD_SET_ZONE:
ESP_LOGV(TAG, "Set zone conf");
this->query_zone_info(); this->query_zone_info();
break; break;
default: default:
break; break;
} }
@ -796,55 +834,57 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
} }
// Read LD2450 buffer data // Read LD2450 buffer data
void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) { void LD2450Component::readline_(int readch) {
if (readch < 0) { if (readch < 0) {
return; return; // No data available
} }
if (this->buffer_pos_ < len - 1) {
buffer[this->buffer_pos_++] = readch; if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
buffer[this->buffer_pos_] = 0; this->buffer_data_[this->buffer_pos_++] = readch;
this->buffer_data_[this->buffer_pos_] = 0;
} else { } else {
// We should never get here, but just in case...
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
this->buffer_pos_ = 0; this->buffer_pos_ = 0;
} }
if (this->buffer_pos_ < 4) { if (this->buffer_pos_ < 4) {
return; return; // Not enough data to process yet
} }
if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) { if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
ESP_LOGV(TAG, "Handle periodic radar data"); this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
this->handle_periodic_data_(buffer, this->buffer_pos_); ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
this->handle_periodic_data_();
this->buffer_pos_ = 0; // Reset position index for next frame this->buffer_pos_ = 0; // Reset position index for next frame
} else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 && } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) { ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
ESP_LOGV(TAG, "Handle command ack data"); if (this->handle_ack_data_()) {
if (this->handle_ack_data_(buffer, this->buffer_pos_)) { this->buffer_pos_ = 0; // Reset position index for next message
this->buffer_pos_ = 0; // Reset position index for next frame
} else { } else {
ESP_LOGV(TAG, "Command ack data invalid"); ESP_LOGV(TAG, "Ack Data incomplete");
} }
} }
} }
// Set Config Mode - Pre-requisite sending commands // Set Config Mode - Pre-requisite sending commands
void LD2450Component::set_config_mode_(bool enable) { void LD2450Component::set_config_mode_(bool enable) {
uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
uint8_t cmd_value[2] = {0x01, 0x00}; const uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(cmd, enable ? cmd_value : nullptr, 2); this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
} }
// Set Bluetooth Enable/Disable // Set Bluetooth Enable/Disable
void LD2450Component::set_bluetooth(bool enable) { void LD2450Component::set_bluetooth(bool enable) {
this->set_config_mode_(true); this->set_config_mode_(true);
uint8_t enable_cmd_value[2] = {0x01, 0x00}; const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
uint8_t disable_cmd_value[2] = {0x00, 0x00}; this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
} }
// Set Baud rate // Set Baud rate
void LD2450Component::set_baud_rate(const std::string &state) { void LD2450Component::set_baud_rate(const std::string &state) {
this->set_config_mode_(true); this->set_config_mode_(true);
uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); }); this->set_timeout(200, [this]() { this->restart_(); });
} }
@ -885,12 +925,12 @@ void LD2450Component::factory_reset() {
void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
// Get LD2450 firmware version // Get LD2450 firmware version
void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
// Get LD2450 mac address // Get LD2450 mac address
void LD2450Component::get_mac_() { void LD2450Component::get_mac_() {
uint8_t cmd_value[2] = {0x01, 0x00}; uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(CMD_MAC, cmd_value, 2); this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
} }
// Query for target tracking mode // Query for target tracking mode

View File

@ -38,10 +38,18 @@ namespace ld2450 {
// Constants // Constants
static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec.
static const uint8_t MAX_LINE_LENGTH = 60; // Max characters for serial buffer static const uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer
static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
enum Direction : uint8_t {
DIRECTION_APPROACHING = 0,
DIRECTION_MOVING_AWAY = 1,
DIRECTION_STATIONARY = 2,
DIRECTION_NA = 3,
DIRECTION_UNDEFINED = 4,
};
// Target coordinate struct // Target coordinate struct
struct Target { struct Target {
int16_t x; int16_t x;
@ -67,19 +75,22 @@ struct ZoneOfNumbers {
#endif #endif
class LD2450Component : public Component, public uart::UARTDevice { class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
SUB_SENSOR(target_count)
SUB_SENSOR(still_target_count)
SUB_SENSOR(moving_target_count)
#endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(target)
SUB_BINARY_SENSOR(moving_target) SUB_BINARY_SENSOR(moving_target)
SUB_BINARY_SENSOR(still_target) SUB_BINARY_SENSOR(still_target)
SUB_BINARY_SENSOR(target)
#endif
#ifdef USE_SENSOR
SUB_SENSOR(moving_target_count)
SUB_SENSOR(still_target_count)
SUB_SENSOR(target_count)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
SUB_TEXT_SENSOR(mac) SUB_TEXT_SENSOR(mac)
SUB_TEXT_SENSOR(version)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(presence_timeout)
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
SUB_SELECT(baud_rate) SUB_SELECT(baud_rate)
@ -90,12 +101,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
SUB_SWITCH(multi_target) SUB_SWITCH(multi_target)
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
SUB_BUTTON(reset) SUB_BUTTON(factory_reset)
SUB_BUTTON(restart) SUB_BUTTON(restart)
#endif #endif
#ifdef USE_NUMBER
SUB_NUMBER(presence_timeout)
#endif
public: public:
void setup() override; void setup() override;
@ -138,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
protected: protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable); void set_config_mode_(bool enable);
void handle_periodic_data_(uint8_t *buffer, uint8_t len); void handle_periodic_data_();
bool handle_ack_data_(uint8_t *buffer, uint8_t len); bool handle_ack_data_();
void process_zone_(uint8_t *buffer); void process_zone_();
void readline_(int readch, uint8_t *buffer, uint8_t len); void readline_(int readch);
void get_version_(); void get_version_();
void get_mac_(); void get_mac_();
void query_target_tracking_mode_(); void query_target_tracking_mode_();
@ -159,13 +167,14 @@ class LD2450Component : public Component, public uart::UARTDevice {
uint32_t moving_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0; uint16_t throttle_ = 0;
uint16_t timeout_ = 5; uint16_t timeout_ = 5;
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t zone_type_ = 0; uint8_t zone_type_ = 0;
bool bluetooth_on_{false};
Target target_info_[MAX_TARGETS]; Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES]; Zone zone_config_[MAX_ZONES];
std::string version_{};
std::string mac_{};
// Change detection - cache previous values to avoid redundant publishes // Change detection - cache previous values to avoid redundant publishes
// All values are initialized to sentinel values that are outside the valid sensor ranges // All values are initialized to sentinel values that are outside the valid sensor ranges
@ -176,8 +185,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
int16_t speed = std::numeric_limits<int16_t>::min(); // -32768, outside practical sensor range int16_t speed = std::numeric_limits<int16_t>::min(); // -32768, outside practical sensor range
uint16_t resolution = std::numeric_limits<uint16_t>::max(); // 65535, unlikely resolution value uint16_t resolution = std::numeric_limits<uint16_t>::max(); // 65535, unlikely resolution value
uint16_t distance = std::numeric_limits<uint16_t>::max(); // 65535, outside range of 0 to ~8990 uint16_t distance = std::numeric_limits<uint16_t>::max(); // 65535, outside range of 0 to ~8990
Direction direction = DIRECTION_UNDEFINED; // Undefined, will differ from any real direction
float angle = NAN; // NAN, safe sentinel for floats float angle = NAN; // NAN, safe sentinel for floats
std::string direction = ""; // Empty string, will differ from any real direction
} cached_target_data_[MAX_TARGETS]; } cached_target_data_[MAX_TARGETS];
struct CachedZoneData { struct CachedZoneData {

View File

@ -0,0 +1,35 @@
#include "esphome/core/helpers.h"
#ifdef USE_LIBRETINY
#include "esphome/core/hal.h"
#include <WiFi.h> // for macAddress()
namespace esphome {
uint32_t random_uint32() { return rand(); }
bool random_bytes(uint8_t *data, size_t len) {
lt_rand_bytes(data, len);
return true;
}
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
Mutex::~Mutex() {}
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
// only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
WiFi.macAddress(mac);
}
} // namespace esphome
#endif // USE_LIBRETINY

View File

@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component {
} }
virtual ESPColorView get_view_internal(int32_t index) const = 0; virtual ESPColorView get_view_internal(int32_t index) const = 0;
bool effect_active_{false};
ESPColorCorrection correction_{}; ESPColorCorrection correction_{};
LightState *state_parent_{nullptr};
#ifdef USE_POWER_SUPPLY #ifdef USE_POWER_SUPPLY
power_supply::PowerSupplyRequester power_; power_supply::PowerSupplyRequester power_;
#endif #endif
LightState *state_parent_{nullptr}; bool effect_active_{false};
}; };
class AddressableLightTransformer : public LightTransitionTransformer { class AddressableLightTransformer : public LightTransitionTransformer {
@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer {
protected: protected:
AddressableLight &light_; AddressableLight &light_;
Color target_color_{};
float last_transition_progress_{0.0f}; float last_transition_progress_{0.0f};
float accumulated_alpha_{0.0f}; float accumulated_alpha_{0.0f};
Color target_color_{};
}; };
} // namespace light } // namespace light

View File

@ -69,8 +69,8 @@ class ESPColorCorrection {
protected: protected:
uint8_t gamma_table_[256]; uint8_t gamma_table_[256];
uint8_t gamma_reverse_table_[256]; uint8_t gamma_reverse_table_[256];
uint8_t local_brightness_{255};
Color max_brightness_; Color max_brightness_;
uint8_t local_brightness_{255};
}; };
} // namespace light } // namespace light

View File

@ -2,12 +2,28 @@
#include "light_call.h" #include "light_call.h"
#include "light_state.h" #include "light_state.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/optional.h"
namespace esphome { namespace esphome {
namespace light { namespace light {
static const char *const TAG = "light"; static const char *const TAG = "light";
// Macro to reduce repetitive setter code
#define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \
LightCall &LightCall::set_##name(optional<type>(name)) { \
if ((name).has_value()) { \
this->name##_ = (name).value(); \
} \
this->set_flag_(flag, (name).has_value()); \
return *this; \
} \
LightCall &LightCall::set_##name(type name) { \
this->name##_ = name; \
this->set_flag_(flag, true); \
return *this; \
}
static const LogString *color_mode_to_human(ColorMode color_mode) { static const LogString *color_mode_to_human(ColorMode color_mode) {
if (color_mode == ColorMode::UNKNOWN) if (color_mode == ColorMode::UNKNOWN)
return LOG_STR("Unknown"); return LOG_STR("Unknown");
@ -32,41 +48,43 @@ void LightCall::perform() {
const char *name = this->parent_->get_name().c_str(); const char *name = this->parent_->get_name().c_str();
LightColorValues v = this->validate_(); LightColorValues v = this->validate_();
if (this->publish_) { if (this->get_publish_()) {
ESP_LOGD(TAG, "'%s' Setting:", name); ESP_LOGD(TAG, "'%s' Setting:", name);
// Only print color mode when it's being changed // Only print color mode when it's being changed
ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); ColorMode current_color_mode = this->parent_->remote_values.get_color_mode();
if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode;
if (target_color_mode != current_color_mode) {
ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode())));
} }
// Only print state when it's being changed // Only print state when it's being changed
bool current_state = this->parent_->remote_values.is_on(); bool current_state = this->parent_->remote_values.is_on();
if (this->state_.value_or(current_state) != current_state) { bool target_state = this->has_state() ? this->state_ : current_state;
if (target_state != current_state) {
ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on()));
} }
if (this->brightness_.has_value()) { if (this->has_brightness()) {
ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f);
} }
if (this->color_brightness_.has_value()) { if (this->has_color_brightness()) {
ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f); ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f);
} }
if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { if (this->has_red() || this->has_green() || this->has_blue()) {
ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
v.get_blue() * 100.0f); v.get_blue() * 100.0f);
} }
if (this->white_.has_value()) { if (this->has_white()) {
ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f);
} }
if (this->color_temperature_.has_value()) { if (this->has_color_temperature()) {
ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature());
} }
if (this->cold_white_.has_value() || this->warm_white_.has_value()) { if (this->has_cold_white() || this->has_warm_white()) {
ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f,
v.get_warm_white() * 100.0f); v.get_warm_white() * 100.0f);
} }
@ -74,58 +92,57 @@ void LightCall::perform() {
if (this->has_flash_()) { if (this->has_flash_()) {
// FLASH // FLASH
if (this->publish_) { if (this->get_publish_()) {
ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f);
} }
this->parent_->start_flash_(v, *this->flash_length_, this->publish_); this->parent_->start_flash_(v, this->flash_length_, this->get_publish_());
} else if (this->has_transition_()) { } else if (this->has_transition_()) {
// TRANSITION // TRANSITION
if (this->publish_) { if (this->get_publish_()) {
ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f);
} }
// Special case: Transition and effect can be set when turning off // Special case: Transition and effect can be set when turning off
if (this->has_effect_()) { if (this->has_effect_()) {
if (this->publish_) { if (this->get_publish_()) {
ESP_LOGD(TAG, " Effect: 'None'"); ESP_LOGD(TAG, " Effect: 'None'");
} }
this->parent_->stop_effect_(); this->parent_->stop_effect_();
} }
this->parent_->start_transition_(v, *this->transition_length_, this->publish_); this->parent_->start_transition_(v, this->transition_length_, this->get_publish_());
} else if (this->has_effect_()) { } else if (this->has_effect_()) {
// EFFECT // EFFECT
auto effect = this->effect_;
const char *effect_s; const char *effect_s;
if (effect == 0u) { if (this->effect_ == 0u) {
effect_s = "None"; effect_s = "None";
} else { } else {
effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str();
} }
if (this->publish_) { if (this->get_publish_()) {
ESP_LOGD(TAG, " Effect: '%s'", effect_s); ESP_LOGD(TAG, " Effect: '%s'", effect_s);
} }
this->parent_->start_effect_(*this->effect_); this->parent_->start_effect_(this->effect_);
// Also set light color values when starting an effect // Also set light color values when starting an effect
// For example to turn off the light // For example to turn off the light
this->parent_->set_immediately_(v, true); this->parent_->set_immediately_(v, true);
} else { } else {
// INSTANT CHANGE // INSTANT CHANGE
this->parent_->set_immediately_(v, this->publish_); this->parent_->set_immediately_(v, this->get_publish_());
} }
if (!this->has_transition_()) { if (!this->has_transition_()) {
this->parent_->target_state_reached_callback_.call(); this->parent_->target_state_reached_callback_.call();
} }
if (this->publish_) { if (this->get_publish_()) {
this->parent_->publish_state(); this->parent_->publish_state();
} }
if (this->save_) { if (this->get_save_()) {
this->parent_->save_remote_values_(); this->parent_->save_remote_values_();
} }
} }
@ -135,82 +152,80 @@ LightColorValues LightCall::validate_() {
auto traits = this->parent_->get_traits(); auto traits = this->parent_->get_traits();
// Color mode check // Color mode check
if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
ESP_LOGW(TAG, "'%s' does not support color mode %s", name, ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); this->set_flag_(FLAG_HAS_COLOR_MODE, false);
this->color_mode_.reset();
} }
// Ensure there is always a color mode set // Ensure there is always a color mode set
if (!this->color_mode_.has_value()) { if (!this->has_color_mode()) {
this->color_mode_ = this->compute_color_mode_(); this->color_mode_ = this->compute_color_mode_();
this->set_flag_(FLAG_HAS_COLOR_MODE, true);
} }
auto color_mode = *this->color_mode_; auto color_mode = this->color_mode_;
// Transform calls that use non-native parameters for the current mode. // Transform calls that use non-native parameters for the current mode.
this->transform_parameters_(); this->transform_parameters_();
// Brightness exists check // Brightness exists check
if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
ESP_LOGW(TAG, "'%s': setting brightness not supported", name); ESP_LOGW(TAG, "'%s': setting brightness not supported", name);
this->brightness_.reset(); this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
} }
// Transition length possible check // Transition length possible check
if (this->transition_length_.has_value() && *this->transition_length_ != 0 && if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
!(color_mode & ColorCapability::BRIGHTNESS)) {
ESP_LOGW(TAG, "'%s': transitions not supported", name); ESP_LOGW(TAG, "'%s': transitions not supported", name);
this->transition_length_.reset(); this->set_flag_(FLAG_HAS_TRANSITION, false);
} }
// Color brightness exists check // Color brightness exists check
if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name);
this->color_brightness_.reset(); this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
} }
// RGB exists check // RGB exists check
if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->blue_.has_value() && *this->blue_ > 0.0f)) { (this->has_blue() && this->blue_ > 0.0f)) {
if (!(color_mode & ColorCapability::RGB)) { if (!(color_mode & ColorCapability::RGB)) {
ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name);
this->red_.reset(); this->set_flag_(FLAG_HAS_RED, false);
this->green_.reset(); this->set_flag_(FLAG_HAS_GREEN, false);
this->blue_.reset(); this->set_flag_(FLAG_HAS_BLUE, false);
} }
} }
// White value exists check // White value exists check
if (this->white_.has_value() && *this->white_ > 0.0f && if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name);
this->white_.reset(); this->set_flag_(FLAG_HAS_WHITE, false);
} }
// Color temperature exists check // Color temperature exists check
if (this->color_temperature_.has_value() && if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name);
this->color_temperature_.reset(); this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
} }
// Cold/warm white value exists check // Cold/warm white value exists check
if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
(this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) {
if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name);
this->cold_white_.reset(); this->set_flag_(FLAG_HAS_COLD_WHITE, false);
this->warm_white_.reset(); this->set_flag_(FLAG_HAS_WARM_WHITE, false);
} }
} }
#define VALIDATE_RANGE_(name_, upper_name, min, max) \ #define VALIDATE_RANGE_(name_, upper_name, min, max) \
if (name_##_.has_value()) { \ if (this->has_##name_()) { \
auto val = *name_##_; \ auto val = this->name_##_; \
if (val < (min) || val > (max)) { \ if (val < (min) || val > (max)) { \
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \
(min), (max)); \ (min), (max)); \
name_##_ = clamp(val, (min), (max)); \ this->name_##_ = clamp(val, (min), (max)); \
} \ } \
} }
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) #define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
@ -227,110 +242,116 @@ LightColorValues LightCall::validate_() {
VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect. // Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
bool explicit_turn_off_request = this->state_.has_value() && !*this->state_; bool explicit_turn_off_request = this->has_state() && !this->state_;
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
if (this->brightness_.has_value() && *this->brightness_ == 0.0f) { if (this->has_brightness() && this->brightness_ == 0.0f) {
this->state_ = optional<float>(false); this->state_ = false;
this->brightness_ = optional<float>(1.0f); this->set_flag_(FLAG_HAS_STATE, true);
this->brightness_ = 1.0f;
} }
// Set color brightness to 100% if currently zero and a color is set. // Set color brightness to 100% if currently zero and a color is set.
if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { if (this->has_red() || this->has_green() || this->has_blue()) {
if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) {
this->color_brightness_ = optional<float>(1.0f); this->color_brightness_ = 1.0f;
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
}
} }
// Create color values for the light with this call applied. // Create color values for the light with this call applied.
auto v = this->parent_->remote_values; auto v = this->parent_->remote_values;
if (this->color_mode_.has_value()) if (this->has_color_mode())
v.set_color_mode(*this->color_mode_); v.set_color_mode(this->color_mode_);
if (this->state_.has_value()) if (this->has_state())
v.set_state(*this->state_); v.set_state(this->state_);
if (this->brightness_.has_value()) if (this->has_brightness())
v.set_brightness(*this->brightness_); v.set_brightness(this->brightness_);
if (this->color_brightness_.has_value()) if (this->has_color_brightness())
v.set_color_brightness(*this->color_brightness_); v.set_color_brightness(this->color_brightness_);
if (this->red_.has_value()) if (this->has_red())
v.set_red(*this->red_); v.set_red(this->red_);
if (this->green_.has_value()) if (this->has_green())
v.set_green(*this->green_); v.set_green(this->green_);
if (this->blue_.has_value()) if (this->has_blue())
v.set_blue(*this->blue_); v.set_blue(this->blue_);
if (this->white_.has_value()) if (this->has_white())
v.set_white(*this->white_); v.set_white(this->white_);
if (this->color_temperature_.has_value()) if (this->has_color_temperature())
v.set_color_temperature(*this->color_temperature_); v.set_color_temperature(this->color_temperature_);
if (this->cold_white_.has_value()) if (this->has_cold_white())
v.set_cold_white(*this->cold_white_); v.set_cold_white(this->cold_white_);
if (this->warm_white_.has_value()) if (this->has_warm_white())
v.set_warm_white(*this->warm_white_); v.set_warm_white(this->warm_white_);
v.normalize_color(); v.normalize_color();
// Flash length check // Flash length check
if (this->has_flash_() && *this->flash_length_ == 0) { if (this->has_flash_() && this->flash_length_ == 0) {
ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name);
this->flash_length_.reset(); this->set_flag_(FLAG_HAS_FLASH, false);
} }
// validate transition length/flash length/effect not used at the same time // validate transition length/flash length/effect not used at the same time
bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; bool supports_transition = color_mode & ColorCapability::BRIGHTNESS;
// If effect is already active, remove effect start // If effect is already active, remove effect start
if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
this->effect_.reset(); this->set_flag_(FLAG_HAS_EFFECT, false);
} }
// validate effect index // validate effect index
if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
this->effect_.reset(); this->set_flag_(FLAG_HAS_EFFECT, false);
} }
if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name);
this->transition_length_.reset(); this->set_flag_(FLAG_HAS_TRANSITION, false);
this->flash_length_.reset(); this->set_flag_(FLAG_HAS_FLASH, false);
} }
if (this->has_flash_() && this->has_transition_()) { if (this->has_flash_() && this->has_transition_()) {
ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name);
this->transition_length_.reset(); this->set_flag_(FLAG_HAS_TRANSITION, false);
} }
if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
supports_transition) { supports_transition) {
// nothing specified and light supports transitions, set default transition length // nothing specified and light supports transitions, set default transition length
this->transition_length_ = this->parent_->default_transition_length_; this->transition_length_ = this->parent_->default_transition_length_;
this->set_flag_(FLAG_HAS_TRANSITION, true);
} }
if (this->transition_length_.value_or(0) == 0) { if (this->has_transition_() && this->transition_length_ == 0) {
// 0 transition is interpreted as no transition (instant change) // 0 transition is interpreted as no transition (instant change)
this->transition_length_.reset(); this->set_flag_(FLAG_HAS_TRANSITION, false);
} }
if (this->has_transition_() && !supports_transition) { if (this->has_transition_() && !supports_transition) {
ESP_LOGW(TAG, "'%s': transitions not supported", name); ESP_LOGW(TAG, "'%s': transitions not supported", name);
this->transition_length_.reset(); this->set_flag_(FLAG_HAS_TRANSITION, false);
} }
// If not a flash and turning the light off, then disable the light // If not a flash and turning the light off, then disable the light
// Do not use light color values directly, so that effects can set 0% brightness // Do not use light color values directly, so that effects can set 0% brightness
// Reason: When user turns off the light in frontend, the effect should also stop // Reason: When user turns off the light in frontend, the effect should also stop
if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { bool target_state = this->has_state() ? this->state_ : v.is_on();
if (!this->has_flash_() && !target_state) {
if (this->has_effect_()) { if (this->has_effect_()) {
ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name);
this->effect_.reset(); this->set_flag_(FLAG_HAS_EFFECT, false);
} else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
// Auto turn off effect // Auto turn off effect
this->effect_ = 0; this->effect_ = 0;
this->set_flag_(FLAG_HAS_EFFECT, true);
} }
} }
// Disable saving for flashes // Disable saving for flashes
if (this->has_flash_()) if (this->has_flash_())
this->save_ = false; this->set_flag_(FLAG_SAVE, false);
return v; return v;
} }
@ -343,24 +364,27 @@ void LightCall::transform_parameters_() {
// - RGBWW lights with color_interlock=true, which also sets "brightness" and // - RGBWW lights with color_interlock=true, which also sets "brightness" and
// "color_temperature" (without color_interlock, CW/WW are set directly) // "color_temperature" (without color_interlock, CW/WW are set directly)
// - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature" // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature"
if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) && //
(*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && //
!(*this->color_mode_ & ColorCapability::WHITE) && // !(this->color_mode_ & ColorCapability::WHITE) && //
!(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && //
traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str()); this->parent_->get_name().c_str());
if (this->color_temperature_.has_value()) { if (this->has_color_temperature()) {
const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds());
const float ww_fraction = const float ww_fraction =
(color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds());
const float cw_fraction = 1.0f - ww_fraction; const float cw_fraction = 1.0f - ww_fraction;
const float max_cw_ww = std::max(ww_fraction, cw_fraction); const float max_cw_ww = std::max(ww_fraction, cw_fraction);
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct());
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct());
this->set_flag_(FLAG_HAS_COLD_WHITE, true);
this->set_flag_(FLAG_HAS_WARM_WHITE, true);
} }
if (this->white_.has_value()) { if (this->has_white()) {
this->brightness_ = *this->white_; this->brightness_ = this->white_;
this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
} }
} }
} }
@ -378,7 +402,7 @@ ColorMode LightCall::compute_color_mode_() {
// Don't change if the light is being turned off. // Don't change if the light is being turned off.
ColorMode current_mode = this->parent_->remote_values.get_color_mode(); ColorMode current_mode = this->parent_->remote_values.get_color_mode();
if (this->state_.has_value() && !*this->state_) if (this->has_state() && !this->state_)
return current_mode; return current_mode;
// If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
@ -411,12 +435,12 @@ ColorMode LightCall::compute_color_mode_() {
return color_mode; return color_mode;
} }
std::set<ColorMode> LightCall::get_suitable_color_modes_() { std::set<ColorMode> LightCall::get_suitable_color_modes_() {
bool has_white = this->white_.has_value() && *this->white_ > 0.0f; bool has_white = this->has_white() && this->white_ > 0.0f;
bool has_ct = this->color_temperature_.has_value(); bool has_ct = this->has_color_temperature();
bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || bool has_cwww =
(this->warm_white_.has_value() && *this->warm_white_ > 0.0f); (this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f);
bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
(this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); (this->has_red() || this->has_green() || this->has_blue());
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
#define ENTRY(white, ct, cwww, rgb, ...) \ #define ENTRY(white, ct, cwww, rgb, ...) \
@ -491,7 +515,7 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) {
return *this; return *this;
} }
ColorMode LightCall::get_active_color_mode_() { ColorMode LightCall::get_active_color_mode_() {
return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); return this->has_color_mode() ? this->color_mode_ : this->parent_->remote_values.get_color_mode();
} }
LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) {
if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS)
@ -505,7 +529,7 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) {
} }
LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) {
if (this->parent_->get_traits().supports_color_mode(color_mode)) if (this->parent_->get_traits().supports_color_mode(color_mode))
this->color_mode_ = color_mode; this->set_color_mode(color_mode);
return *this; return *this;
} }
LightCall &LightCall::set_color_brightness_if_supported(float brightness) { LightCall &LightCall::set_color_brightness_if_supported(float brightness) {
@ -549,110 +573,19 @@ LightCall &LightCall::set_warm_white_if_supported(float warm_white) {
this->set_warm_white(warm_white); this->set_warm_white(warm_white);
return *this; return *this;
} }
LightCall &LightCall::set_state(optional<bool> state) { IMPLEMENT_LIGHT_CALL_SETTER(state, bool, FLAG_HAS_STATE)
this->state_ = state; IMPLEMENT_LIGHT_CALL_SETTER(transition_length, uint32_t, FLAG_HAS_TRANSITION)
return *this; IMPLEMENT_LIGHT_CALL_SETTER(flash_length, uint32_t, FLAG_HAS_FLASH)
} IMPLEMENT_LIGHT_CALL_SETTER(brightness, float, FLAG_HAS_BRIGHTNESS)
LightCall &LightCall::set_state(bool state) { IMPLEMENT_LIGHT_CALL_SETTER(color_mode, ColorMode, FLAG_HAS_COLOR_MODE)
this->state_ = state; IMPLEMENT_LIGHT_CALL_SETTER(color_brightness, float, FLAG_HAS_COLOR_BRIGHTNESS)
return *this; IMPLEMENT_LIGHT_CALL_SETTER(red, float, FLAG_HAS_RED)
} IMPLEMENT_LIGHT_CALL_SETTER(green, float, FLAG_HAS_GREEN)
LightCall &LightCall::set_transition_length(optional<uint32_t> transition_length) { IMPLEMENT_LIGHT_CALL_SETTER(blue, float, FLAG_HAS_BLUE)
this->transition_length_ = transition_length; IMPLEMENT_LIGHT_CALL_SETTER(white, float, FLAG_HAS_WHITE)
return *this; IMPLEMENT_LIGHT_CALL_SETTER(color_temperature, float, FLAG_HAS_COLOR_TEMPERATURE)
} IMPLEMENT_LIGHT_CALL_SETTER(cold_white, float, FLAG_HAS_COLD_WHITE)
LightCall &LightCall::set_transition_length(uint32_t transition_length) { IMPLEMENT_LIGHT_CALL_SETTER(warm_white, float, FLAG_HAS_WARM_WHITE)
this->transition_length_ = transition_length;
return *this;
}
LightCall &LightCall::set_flash_length(optional<uint32_t> flash_length) {
this->flash_length_ = flash_length;
return *this;
}
LightCall &LightCall::set_flash_length(uint32_t flash_length) {
this->flash_length_ = flash_length;
return *this;
}
LightCall &LightCall::set_brightness(optional<float> brightness) {
this->brightness_ = brightness;
return *this;
}
LightCall &LightCall::set_brightness(float brightness) {
this->brightness_ = brightness;
return *this;
}
LightCall &LightCall::set_color_mode(optional<ColorMode> color_mode) {
this->color_mode_ = color_mode;
return *this;
}
LightCall &LightCall::set_color_mode(ColorMode color_mode) {
this->color_mode_ = color_mode;
return *this;
}
LightCall &LightCall::set_color_brightness(optional<float> brightness) {
this->color_brightness_ = brightness;
return *this;
}
LightCall &LightCall::set_color_brightness(float brightness) {
this->color_brightness_ = brightness;
return *this;
}
LightCall &LightCall::set_red(optional<float> red) {
this->red_ = red;
return *this;
}
LightCall &LightCall::set_red(float red) {
this->red_ = red;
return *this;
}
LightCall &LightCall::set_green(optional<float> green) {
this->green_ = green;
return *this;
}
LightCall &LightCall::set_green(float green) {
this->green_ = green;
return *this;
}
LightCall &LightCall::set_blue(optional<float> blue) {
this->blue_ = blue;
return *this;
}
LightCall &LightCall::set_blue(float blue) {
this->blue_ = blue;
return *this;
}
LightCall &LightCall::set_white(optional<float> white) {
this->white_ = white;
return *this;
}
LightCall &LightCall::set_white(float white) {
this->white_ = white;
return *this;
}
LightCall &LightCall::set_color_temperature(optional<float> color_temperature) {
this->color_temperature_ = color_temperature;
return *this;
}
LightCall &LightCall::set_color_temperature(float color_temperature) {
this->color_temperature_ = color_temperature;
return *this;
}
LightCall &LightCall::set_cold_white(optional<float> cold_white) {
this->cold_white_ = cold_white;
return *this;
}
LightCall &LightCall::set_cold_white(float cold_white) {
this->cold_white_ = cold_white;
return *this;
}
LightCall &LightCall::set_warm_white(optional<float> warm_white) {
this->warm_white_ = warm_white;
return *this;
}
LightCall &LightCall::set_warm_white(float warm_white) {
this->warm_white_ = warm_white;
return *this;
}
LightCall &LightCall::set_effect(optional<std::string> effect) { LightCall &LightCall::set_effect(optional<std::string> effect) {
if (effect.has_value()) if (effect.has_value())
this->set_effect(*effect); this->set_effect(*effect);
@ -660,18 +593,22 @@ LightCall &LightCall::set_effect(optional<std::string> effect) {
} }
LightCall &LightCall::set_effect(uint32_t effect_number) { LightCall &LightCall::set_effect(uint32_t effect_number) {
this->effect_ = effect_number; this->effect_ = effect_number;
this->set_flag_(FLAG_HAS_EFFECT, true);
return *this; return *this;
} }
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) { LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {
this->effect_ = effect_number; if (effect_number.has_value()) {
this->effect_ = effect_number.value();
}
this->set_flag_(FLAG_HAS_EFFECT, effect_number.has_value());
return *this; return *this;
} }
LightCall &LightCall::set_publish(bool publish) { LightCall &LightCall::set_publish(bool publish) {
this->publish_ = publish; this->set_flag_(FLAG_PUBLISH, publish);
return *this; return *this;
} }
LightCall &LightCall::set_save(bool save) { LightCall &LightCall::set_save(bool save) {
this->save_ = save; this->set_flag_(FLAG_SAVE, save);
return *this; return *this;
} }
LightCall &LightCall::set_rgb(float red, float green, float blue) { LightCall &LightCall::set_rgb(float red, float green, float blue) {

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include "esphome/core/optional.h"
#include "light_color_values.h" #include "light_color_values.h"
#include <set> #include <set>
@ -10,6 +9,11 @@ namespace light {
class LightState; class LightState;
/** This class represents a requested change in a light state. /** This class represents a requested change in a light state.
*
* Light state changes are tracked using a bitfield flags_ to minimize memory usage.
* Each possible light property has a flag indicating whether it has been set.
* This design keeps LightCall at ~56 bytes to minimize heap fragmentation on
* ESP8266 and other memory-constrained devices.
*/ */
class LightCall { class LightCall {
public: public:
@ -131,6 +135,19 @@ class LightCall {
/// Set whether this light call should trigger a save state to recover them at startup.. /// Set whether this light call should trigger a save state to recover them at startup..
LightCall &set_save(bool save); LightCall &set_save(bool save);
// Getter methods to check if values are set
bool has_state() const { return (flags_ & FLAG_HAS_STATE) != 0; }
bool has_brightness() const { return (flags_ & FLAG_HAS_BRIGHTNESS) != 0; }
bool has_color_brightness() const { return (flags_ & FLAG_HAS_COLOR_BRIGHTNESS) != 0; }
bool has_red() const { return (flags_ & FLAG_HAS_RED) != 0; }
bool has_green() const { return (flags_ & FLAG_HAS_GREEN) != 0; }
bool has_blue() const { return (flags_ & FLAG_HAS_BLUE) != 0; }
bool has_white() const { return (flags_ & FLAG_HAS_WHITE) != 0; }
bool has_color_temperature() const { return (flags_ & FLAG_HAS_COLOR_TEMPERATURE) != 0; }
bool has_cold_white() const { return (flags_ & FLAG_HAS_COLD_WHITE) != 0; }
bool has_warm_white() const { return (flags_ & FLAG_HAS_WARM_WHITE) != 0; }
bool has_color_mode() const { return (flags_ & FLAG_HAS_COLOR_MODE) != 0; }
/** Set the RGB color of the light by RGB values. /** Set the RGB color of the light by RGB values.
* *
* Please note that this only changes the color of the light, not the brightness. * Please note that this only changes the color of the light, not the brightness.
@ -170,27 +187,62 @@ class LightCall {
/// Some color modes also can be set using non-native parameters, transform those calls. /// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_(); void transform_parameters_();
bool has_transition_() { return this->transition_length_.has_value(); } // Bitfield flags - each flag indicates whether a corresponding value has been set.
bool has_flash_() { return this->flash_length_.has_value(); } enum FieldFlags : uint16_t {
bool has_effect_() { return this->effect_.has_value(); } FLAG_HAS_STATE = 1 << 0,
FLAG_HAS_TRANSITION = 1 << 1,
FLAG_HAS_FLASH = 1 << 2,
FLAG_HAS_EFFECT = 1 << 3,
FLAG_HAS_BRIGHTNESS = 1 << 4,
FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5,
FLAG_HAS_RED = 1 << 6,
FLAG_HAS_GREEN = 1 << 7,
FLAG_HAS_BLUE = 1 << 8,
FLAG_HAS_WHITE = 1 << 9,
FLAG_HAS_COLOR_TEMPERATURE = 1 << 10,
FLAG_HAS_COLD_WHITE = 1 << 11,
FLAG_HAS_WARM_WHITE = 1 << 12,
FLAG_HAS_COLOR_MODE = 1 << 13,
FLAG_PUBLISH = 1 << 14,
FLAG_SAVE = 1 << 15,
};
bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag
void set_flag_(FieldFlags flag, bool value) {
if (value) {
this->flags_ |= flag;
} else {
this->flags_ &= ~flag;
}
}
LightState *parent_; LightState *parent_;
optional<bool> state_;
optional<uint32_t> transition_length_; // Light state values - use flags_ to check if a value has been set.
optional<uint32_t> flash_length_; // Group 4-byte aligned members first
optional<ColorMode> color_mode_; uint32_t transition_length_;
optional<float> brightness_; uint32_t flash_length_;
optional<float> color_brightness_; uint32_t effect_;
optional<float> red_; float brightness_;
optional<float> green_; float color_brightness_;
optional<float> blue_; float red_;
optional<float> white_; float green_;
optional<float> color_temperature_; float blue_;
optional<float> cold_white_; float white_;
optional<float> warm_white_; float color_temperature_;
optional<uint32_t> effect_; float cold_white_;
bool publish_{true}; float warm_white_;
bool save_{true};
// Smaller members at the end for better packing
uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set
ColorMode color_mode_;
bool state_;
}; };
} // namespace light } // namespace light

View File

@ -46,8 +46,7 @@ class LightColorValues {
public: public:
/// Construct the LightColorValues with all attributes enabled, but state set to off. /// Construct the LightColorValues with all attributes enabled, but state set to off.
LightColorValues() LightColorValues()
: color_mode_(ColorMode::UNKNOWN), : state_(0.0f),
state_(0.0f),
brightness_(1.0f), brightness_(1.0f),
color_brightness_(1.0f), color_brightness_(1.0f),
red_(1.0f), red_(1.0f),
@ -56,7 +55,8 @@ class LightColorValues {
white_(1.0f), white_(1.0f),
color_temperature_{0.0f}, color_temperature_{0.0f},
cold_white_{1.0f}, cold_white_{1.0f},
warm_white_{1.0f} {} warm_white_{1.0f},
color_mode_(ColorMode::UNKNOWN) {}
LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green,
float blue, float white, float color_temperature, float cold_white, float warm_white) { float blue, float white, float color_temperature, float cold_white, float warm_white) {
@ -292,7 +292,6 @@ class LightColorValues {
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
protected: protected:
ColorMode color_mode_;
float state_; ///< ON / OFF, float for transition float state_; ///< ON / OFF, float for transition
float brightness_; float brightness_;
float color_brightness_; float color_brightness_;
@ -303,6 +302,7 @@ class LightColorValues {
float color_temperature_; ///< Color Temperature in Mired float color_temperature_; ///< Color Temperature in Mired
float cold_white_; float cold_white_;
float warm_white_; float warm_white_;
ColorMode color_mode_;
}; };
} // namespace light } // namespace light

View File

@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t {
struct LightStateRTCState { struct LightStateRTCState {
LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green, LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green,
float blue, float white, float color_temp, float cold_white, float warm_white) float blue, float white, float color_temp, float cold_white, float warm_white)
: color_mode(color_mode), : brightness(brightness),
state(state),
brightness(brightness),
color_brightness(color_brightness), color_brightness(color_brightness),
red(red), red(red),
green(green), green(green),
@ -41,10 +39,12 @@ struct LightStateRTCState {
white(white), white(white),
color_temp(color_temp), color_temp(color_temp),
cold_white(cold_white), cold_white(cold_white),
warm_white(warm_white) {} warm_white(warm_white),
effect(0),
color_mode(color_mode),
state(state) {}
LightStateRTCState() = default; LightStateRTCState() = default;
ColorMode color_mode{ColorMode::UNKNOWN}; // Group 4-byte aligned members first
bool state{false};
float brightness{1.0f}; float brightness{1.0f};
float color_brightness{1.0f}; float color_brightness{1.0f};
float red{1.0f}; float red{1.0f};
@ -55,6 +55,9 @@ struct LightStateRTCState {
float cold_white{1.0f}; float cold_white{1.0f};
float warm_white{1.0f}; float warm_white{1.0f};
uint32_t effect{0}; uint32_t effect{0};
// Group smaller members at the end
ColorMode color_mode{ColorMode::UNKNOWN};
bool state{false};
}; };
/** This class represents the communication layer between the front-end MQTT layer and the /** This class represents the communication layer between the front-end MQTT layer and the
@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component {
std::unique_ptr<LightTransformer> transformer_{nullptr}; std::unique_ptr<LightTransformer> transformer_{nullptr};
/// List of effects for this light. /// List of effects for this light.
std::vector<LightEffect *> effects_; std::vector<LightEffect *> effects_;
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
/// Value for storing the index of the currently active effect. 0 if no effect is active /// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{}; uint32_t active_effect_index_{};
/// Default transition length for all transitions in ms. /// Default transition length for all transitions in ms.
@ -224,15 +229,11 @@ class LightState : public EntityBase, public Component {
uint32_t flash_transition_length_{}; uint32_t flash_transition_length_{};
/// Gamma correction factor for the light. /// Gamma correction factor for the light.
float gamma_correct_{}; float gamma_correct_{};
/// Whether the light value should be written in the next cycle. /// Whether the light value should be written in the next cycle.
bool next_write_{true}; bool next_write_{true};
// for effects, true if a transformer (transition) is active. // for effects, true if a transformer (transition) is active.
bool is_transformer_active_ = false; bool is_transformer_active_ = false;
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
/** Callback to call when new values for the frontend are available. /** Callback to call when new values for the frontend are available.
* *
* "Remote values" are light color values that are reported to the frontend and have a lower * "Remote values" are light color values that are reported to the frontend and have a lower

View File

@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer {
// transition from 0 to 1 on x = [0, 1] // transition from 0 to 1 on x = [0, 1]
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
bool changing_color_mode_{false};
LightColorValues end_values_{}; LightColorValues end_values_{};
LightColorValues intermediate_values_{}; LightColorValues intermediate_values_{};
bool changing_color_mode_{false};
}; };
class LightFlashTransformer : public LightTransformer { class LightFlashTransformer : public LightTransformer {
@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer {
protected: protected:
LightState &state_; LightState &state_;
uint32_t transition_length_;
std::unique_ptr<LightTransformer> transformer_{nullptr}; std::unique_ptr<LightTransformer> transformer_{nullptr};
uint32_t transition_length_;
bool begun_lightstate_restore_; bool begun_lightstate_restore_;
}; };

View File

@ -21,6 +21,7 @@ from esphome.components.libretiny.const import (
COMPONENT_LN882X, COMPONENT_LN882X,
COMPONENT_RTL87XX, COMPONENT_RTL87XX,
) )
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ARGS, CONF_ARGS,
@ -42,6 +43,7 @@ from esphome.const import (
PLATFORM_LN882X, PLATFORM_LN882X,
PLATFORM_RP2040, PLATFORM_RP2040,
PLATFORM_RTL87XX, PLATFORM_RTL87XX,
PlatformFramework,
) )
from esphome.core import CORE, Lambda, coroutine_with_priority from esphome.core import CORE, Lambda, coroutine_with_priority
@ -444,3 +446,25 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_) return cg.new_Pvariable(action_id, template_arg, lambda_)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"logger_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"logger_host.cpp": {PlatformFramework.HOST_NATIVE},
"logger_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"logger_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"task_log_buffer.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
}
)

View File

@ -90,6 +90,25 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
// Implementation for ESP8266 with flash string support. // Implementation for ESP8266 with flash string support.
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
//
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
// The buffer is used in a special way to avoid allocating extra memory:
//
// Memory layout during execution:
// Step 1: Copy format string from flash to buffer
// tx_buffer_: [format_string][null][.....................]
// tx_buffer_at_: ------------------^
// msg_start: saved here -----------^
//
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
// and writes formatted output starting at msg_start position
// tx_buffer_: [format_string][null][formatted_message][null]
// tx_buffer_at_: -------------------------------------^
//
// Step 3: Output the formatted message (starting at msg_start)
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
// which points to: [formatted_message][null]
//
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT va_list args) { // NOLINT
if (level > this->level_for(tag) || global_recursion_guard_) if (level > this->level_for(tag) || global_recursion_guard_)
@ -121,7 +140,9 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
if (this->baud_rate_ > 0) { if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start); this->write_msg_(this->tx_buffer_ + msg_start);
} }
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start); size_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
global_recursion_guard_ = false; global_recursion_guard_ = false;
} }
@ -185,7 +206,8 @@ void Logger::loop() {
this->tx_buffer_size_); this->tx_buffer_size_);
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
this->tx_buffer_[this->tx_buffer_at_] = '\0'; this->tx_buffer_[this->tx_buffer_at_] = '\0';
this->log_callback_.call(message->level, message->tag, this->tx_buffer_); size_t msg_len = this->tx_buffer_at_; // We already know the length from tx_buffer_at_
this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len);
// At this point all the data we need from message has been transferred to the tx_buffer // At this point all the data we need from message has been transferred to the tx_buffer
// so we can release the message to allow other tasks to use it as soon as possible. // so we can release the message to allow other tasks to use it as soon as possible.
this->log_buffer_->release_message_main_loop(received_token); this->log_buffer_->release_message_main_loop(received_token);
@ -214,7 +236,7 @@ void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->lo
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
#endif #endif
void Logger::add_on_log_callback(std::function<void(uint8_t, const char *, const char *)> &&callback) { void Logger::add_on_log_callback(std::function<void(uint8_t, const char *, const char *, size_t)> &&callback) {
this->log_callback_.add(std::move(callback)); this->log_callback_.add(std::move(callback));
} }
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }

View File

@ -143,7 +143,7 @@ class Logger : public Component {
inline uint8_t level_for(const char *tag); inline uint8_t level_for(const char *tag);
/// Register a callback that will be called for every log message sent /// Register a callback that will be called for every log message sent
void add_on_log_callback(std::function<void(uint8_t, const char *, const char *)> &&callback); void add_on_log_callback(std::function<void(uint8_t, const char *, const char *, size_t)> &&callback);
// add a listener for log level changes // add a listener for log level changes
void add_listener(std::function<void(uint8_t)> &&callback) { this->level_callback_.add(std::move(callback)); } void add_listener(std::function<void(uint8_t)> &&callback) { this->level_callback_.add(std::move(callback)); }
@ -192,7 +192,7 @@ class Logger : public Component {
if (this->baud_rate_ > 0) { if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
} }
this->log_callback_.call(level, tag, this->tx_buffer_); this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
} }
// Write the body of the log message to the buffer // Write the body of the log message to the buffer
@ -246,7 +246,7 @@ class Logger : public Component {
// Large objects (internally aligned) // Large objects (internally aligned)
std::map<std::string, uint8_t> log_levels_{}; std::map<std::string, uint8_t> log_levels_{};
CallbackManager<void(uint8_t, const char *, const char *)> log_callback_{}; CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
CallbackManager<void(uint8_t)> level_callback_{}; CallbackManager<void(uint8_t)> level_callback_{};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
@ -355,7 +355,7 @@ class Logger : public Component {
} }
inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
} }
@ -385,7 +385,7 @@ class LoggerMessageTrigger : public Trigger<uint8_t, const char *, const char *>
public: public:
explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { explicit LoggerMessageTrigger(Logger *parent, uint8_t level) {
this->level_ = level; this->level_ = level;
parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message) { parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) {
if (level <= this->level_) { if (level <= this->level_) {
this->trigger(level, tag, message); this->trigger(level, tag, message);
} }

View File

@ -184,7 +184,9 @@ void HOT Logger::write_msg_(const char *msg) {
) { ) {
puts(msg); puts(msg);
} else { } else {
uart_write_bytes(this->uart_num_, msg, strlen(msg)); // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
uart_write_bytes(this->uart_num_, msg, len);
uart_write_bytes(this->uart_num_, "\n", 1); uart_write_bytes(this->uart_num_, "\n", 1);
} }
} }

View File

View File

@ -0,0 +1,75 @@
#include "lps22.h"
namespace esphome {
namespace lps22 {
static constexpr const char *const TAG = "lps22";
static constexpr uint8_t WHO_AM_I = 0x0F;
static constexpr uint8_t LPS22HB_ID = 0xB1;
static constexpr uint8_t LPS22HH_ID = 0xB3;
static constexpr uint8_t CTRL_REG2 = 0x11;
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
static constexpr uint8_t STATUS = 0x27;
static constexpr uint8_t STATUS_T_DA_MASK = 0b10;
static constexpr uint8_t STATUS_P_DA_MASK = 0b01;
static constexpr uint8_t TEMP_L = 0x2b;
static constexpr uint8_t PRES_OUT_XL = 0x28;
static constexpr uint8_t REF_P_XL = 0x28;
static constexpr uint8_t READ_ATTEMPTS = 10;
static constexpr uint8_t READ_INTERVAL = 5;
static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f;
static constexpr float TEMPERATURE_SCALE = 0.01f;
void LPS22Component::setup() {
uint8_t value = 0x00;
this->read_register(WHO_AM_I, &value, 1);
if (value != LPS22HB_ID && value != LPS22HH_ID) {
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
this->mark_failed();
}
}
void LPS22Component::dump_config() {
ESP_LOGCONFIG(TAG, "LPS22:");
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
void LPS22Component::update() {
uint8_t value = 0x00;
this->read_register(CTRL_REG2, &value, 1);
value |= CTRL_REG2_ONE_SHOT_MASK;
this->write_register(CTRL_REG2, &value, 1);
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
}
RetryResult LPS22Component::try_read_() {
uint8_t value = 0x00;
this->read_register(STATUS, &value, 1);
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
if ((value & expected_status_mask) != expected_status_mask) {
ESP_LOGD(TAG, "STATUS not ready: %x", value);
return RetryResult::RETRY;
}
if (this->temperature_sensor_ != nullptr) {
uint8_t t_buf[2]{0};
this->read_register(TEMP_L, t_buf, 2);
int16_t encoded = static_cast<int16_t>(encode_uint16(t_buf[1], t_buf[0]));
float temp = TEMPERATURE_SCALE * static_cast<float>(encoded);
this->temperature_sensor_->publish_state(temp);
}
if (this->pressure_sensor_ != nullptr) {
uint8_t p_buf[3]{0};
this->read_register(PRES_OUT_XL, p_buf, 3);
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
}
return RetryResult::DONE;
}
} // namespace lps22
} // namespace esphome

View File

@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace lps22 {
class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
void setup() override;
void update() override;
void dump_config() override;
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
RetryResult try_read_();
};
} // namespace lps22
} // namespace esphome

View File

@ -0,0 +1,58 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
CONF_TEMPERATURE,
CONF_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
ICON_THERMOMETER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
)
CODEOWNERS = ["@nagisa"]
DEPENDENCIES = ["i2c"]
lps22 = cg.esphome_ns.namespace("lps22")
LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LPS22Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x5D)) # can also be 0x5C
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))

View File

@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DISABLED, CONF_DISABLED,
@ -8,6 +9,7 @@ from esphome.const import (
CONF_PROTOCOL, CONF_PROTOCOL,
CONF_SERVICE, CONF_SERVICE,
CONF_SERVICES, CONF_SERVICES,
PlatformFramework,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -108,3 +110,21 @@ async def to_code(config):
) )
cg.add(var.add_extra_service(exp)) cg.add(var.add_extra_service(exp))
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"mdns_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"mdns_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"mdns_host.cpp": {PlatformFramework.HOST_NATIVE},
"mdns_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"mdns_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -5,6 +5,7 @@ from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import logger from esphome.components import logger
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
@ -54,6 +55,7 @@ from esphome.const import (
PLATFORM_BK72XX, PLATFORM_BK72XX,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PlatformFramework,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -596,3 +598,13 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args):
async def mqtt_disable_to_code(config, action_id, template_arg, args): async def mqtt_disable_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"mqtt_backend_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
}
)

View File

@ -57,14 +57,15 @@ void MQTTClientComponent::setup() {
}); });
#ifdef USE_LOGGER #ifdef USE_LOGGER
if (this->is_log_message_enabled() && logger::global_logger != nullptr) { if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { logger::global_logger->add_on_log_callback(
if (level <= this->log_level_ && this->is_connected()) { [this](int level, const char *tag, const char *message, size_t message_len) {
this->publish({.topic = this->log_message_.topic, if (level <= this->log_level_ && this->is_connected()) {
.payload = message, this->publish({.topic = this->log_message_.topic,
.qos = this->log_message_.qos, .payload = std::string(message, message_len),
.retain = this->log_message_.retain}); .qos = this->log_message_.qos,
} .retain = this->log_message_.retain});
}); }
});
} }
#endif #endif

View File

@ -1,5 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import uart from esphome.components import uart
from esphome.config_helpers import filter_source_files_from_platform
from esphome.const import PlatformFramework
nextion_ns = cg.esphome_ns.namespace("nextion") nextion_ns = cg.esphome_ns.namespace("nextion")
Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice)
@ -8,3 +10,17 @@ nextion_ref = Nextion.operator("ref")
CONF_NEXTION_ID = "nextion_id" CONF_NEXTION_ID = "nextion_id"
CONF_PUBLISH_STATE = "publish_state" CONF_PUBLISH_STATE = "publish_state"
CONF_SEND_TO_NEXTION = "send_to_nextion" CONF_SEND_TO_NEXTION = "send_to_nextion"
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"nextion_upload_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF},
}
)

View File

@ -11,6 +11,7 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
CONF_COMMAND_SPACING = "command_spacing" CONF_COMMAND_SPACING = "command_spacing"
CONF_COMPONENT_NAME = "component_name" CONF_COMPONENT_NAME = "component_name"
CONF_DUMP_DEVICE_INFO = "dump_device_info"
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
CONF_FONT_ID = "font_id" CONF_FONT_ID = "font_id"
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"

View File

@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
return; return;
if (send_to_nextion) { if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) { if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true; this->needs_to_send_update_ = true;
} else { } else {
this->needs_to_send_update_ = false; this->needs_to_send_update_ = false;

View File

@ -15,6 +15,7 @@ from . import Nextion, nextion_ns, nextion_ref
from .base_component import ( from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH, CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING, CONF_COMMAND_SPACING,
CONF_DUMP_DEVICE_INFO,
CONF_EXIT_REPARSE_ON_START, CONF_EXIT_REPARSE_ON_START,
CONF_MAX_COMMANDS_PER_LOOP, CONF_MAX_COMMANDS_PER_LOOP,
CONF_MAX_QUEUE_SIZE, CONF_MAX_QUEUE_SIZE,
@ -57,6 +58,7 @@ CONFIG_SCHEMA = (
cv.positive_time_period_milliseconds, cv.positive_time_period_milliseconds,
cv.Range(max=TimePeriod(milliseconds=255)), cv.Range(max=TimePeriod(milliseconds=255)),
), ),
cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
@ -95,7 +97,9 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_TFT_URL): cv.url,
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any(
0, cv.int_range(min=3, max=65535)
),
cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t, cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t,
} }
) )
@ -172,9 +176,14 @@ async def to_code(config):
cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START])) if config[CONF_DUMP_DEVICE_INFO]:
cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO")
cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) if config[CONF_EXIT_REPARSE_ON_START]:
cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START")
if config[CONF_SKIP_CONNECTION_HANDSHAKE]:
cg.add_define("USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE")
if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP): if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP):
cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP") cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP")

View File

@ -13,14 +13,11 @@ void Nextion::setup() {
this->is_setup_ = false; this->is_setup_ = false;
this->connection_state_.ignore_is_setup_ = true; this->connection_state_.ignore_is_setup_ = true;
// Wake up the nextion // Wake up the nextion and ensure clean communication state
this->send_command_("bkcmd=0"); this->send_command_("sleep=0"); // Exit sleep mode if sleeping
this->send_command_("sleep=0"); this->send_command_("bkcmd=0"); // Disable return data during init sequence
this->send_command_("bkcmd=0"); // Reset device for clean state - critical for reliable communication
this->send_command_("sleep=0");
// Reboot it
this->send_command_("rest"); this->send_command_("rest");
this->connection_state_.ignore_is_setup_ = false; this->connection_state_.ignore_is_setup_ = false;
@ -51,24 +48,19 @@ bool Nextion::check_connect_() {
if (this->connection_state_.is_connected_) if (this->connection_state_.is_connected_)
return true; return true;
// Check if the handshake should be skipped for the Nextion connection #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
if (this->skip_connection_handshake_) { ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake
// Log the connection status without handshake this->is_connected_ = true; // Set the connection status to true
ESP_LOGW(TAG, "Connected (no handshake)"); return true; // Return true indicating the connection is set
// Set the connection status to true #else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
this->connection_state_.is_connected_ = true;
// Return true indicating the connection is set
return true;
}
if (this->comok_sent_ == 0) { if (this->comok_sent_ == 0) {
this->reset_(false); this->reset_(false);
this->connection_state_.ignore_is_setup_ = true; this->connection_state_.ignore_is_setup_ = true;
this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating
if (this->exit_reparse_on_start_) { #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
} #endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
this->send_command_("connect"); this->send_command_("connect");
this->comok_sent_ = App.get_loop_component_start_time(); this->comok_sent_ = App.get_loop_component_start_time();
@ -94,7 +86,7 @@ bool Nextion::check_connect_() {
for (size_t i = 0; i < response.length(); i++) { for (size_t i = 0; i < response.length(); i++) {
ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]); ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]);
} }
#endif #endif // NEXTION_PROTOCOL_LOG
ESP_LOGW(TAG, "Not connected"); ESP_LOGW(TAG, "Not connected");
comok_sent_ = 0; comok_sent_ = 0;
@ -118,11 +110,19 @@ bool Nextion::check_connect_() {
this->is_detected_ = (connect_info.size() == 7); this->is_detected_ = (connect_info.size() == 7);
if (this->is_detected_) { if (this->is_detected_) {
ESP_LOGN(TAG, "Connect info: %zu", connect_info.size()); ESP_LOGN(TAG, "Connect info: %zu", connect_info.size());
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
this->device_model_ = connect_info[2]; this->device_model_ = connect_info[2];
this->firmware_version_ = connect_info[3]; this->firmware_version_ = connect_info[3];
this->serial_number_ = connect_info[5]; this->serial_number_ = connect_info[5];
this->flash_size_ = connect_info[6]; this->flash_size_ = connect_info[6];
#else // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
ESP_LOGI(TAG,
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n",
connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str());
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
} else { } else {
ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str()); ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
} }
@ -130,6 +130,7 @@ bool Nextion::check_connect_() {
this->connection_state_.ignore_is_setup_ = false; this->connection_state_.ignore_is_setup_ = false;
this->dump_config(); this->dump_config();
return true; return true;
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
} }
void Nextion::reset_(bool reset_nextion) { void Nextion::reset_(bool reset_nextion) {
@ -144,29 +145,33 @@ void Nextion::reset_(bool reset_nextion) {
void Nextion::dump_config() { void Nextion::dump_config() {
ESP_LOGCONFIG(TAG, "Nextion:"); ESP_LOGCONFIG(TAG, "Nextion:");
if (this->skip_connection_handshake_) {
ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_)); #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
} else { ESP_LOGCONFIG(TAG, " Skip handshake: YES");
ESP_LOGCONFIG(TAG, #else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s",
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str());
}
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n"
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Exit reparse: YES\n"
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Wake On Touch: %s\n" " Wake On Touch: %s\n"
" Exit reparse: %s", " Touch Timeout: %" PRIu16,
YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_)); #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str(),
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->max_commands_per_loop_); ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->max_commands_per_loop_);
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->touch_sleep_timeout_ != 0) {
ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu16, this->touch_sleep_timeout_);
}
if (this->wake_up_page_ != 255) { if (this->wake_up_page_ != 255) {
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_); ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
} }
@ -314,6 +319,10 @@ void Nextion::loop() {
this->set_wake_up_page(this->wake_up_page_); this->set_wake_up_page(this->wake_up_page_);
} }
if (this->touch_sleep_timeout_ != 0) {
this->set_touch_sleep_timeout(this->touch_sleep_timeout_);
}
this->connection_state_.ignore_is_setup_ = false; this->connection_state_.ignore_is_setup_ = false;
} }

View File

@ -932,21 +932,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/ */
void set_backlight_brightness(float brightness); void set_backlight_brightness(float brightness);
/**
* Sets whether the Nextion display should skip the connection handshake process.
* @param skip_handshake True or false. When skip_connection_handshake is true,
* the connection will be established without performing the handshake.
* This can be useful when using Nextion Simulator.
*
* Example:
* ```cpp
* it.set_skip_connection_handshake(true);
* ```
*
* When set to true, the display will be marked as connected without performing a handshake.
*/
void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; }
/** /**
* Sets Nextion mode between sleep and awake * Sets Nextion mode between sleep and awake
* @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode.
@ -1179,18 +1164,39 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void update_components_by_prefix(const std::string &prefix); void update_components_by_prefix(const std::string &prefix);
/** /**
* Set the touch sleep timeout of the display. * Set the touch sleep timeout of the display using the `thsp` command.
* @param timeout Timeout in seconds. *
* Sets internal No-touch-then-sleep timer to specified value in seconds.
* Nextion will auto-enter sleep mode if and when this timer expires.
*
* @param touch_sleep_timeout Timeout in seconds.
* Range: 3 to 65535 seconds (minimum 3 seconds, maximum ~18 hours 12 minutes 15 seconds)
* Use 0 to disable touch sleep timeout.
*
* @note Once `thsp` is set, it will persist until reboot or reset. The Nextion device
* needs to exit sleep mode to issue `thsp=0` to disable sleep on no touch.
*
* @note The display will only wake up by a restart or by setting up `thup` (auto wake on touch).
* See set_auto_wake_on_touch() to configure wake behavior.
* *
* Example: * Example:
* ```cpp * ```cpp
* // Set 30 second touch timeout
* it.set_touch_sleep_timeout(30); * it.set_touch_sleep_timeout(30);
*
* // Set maximum timeout (~18 hours)
* it.set_touch_sleep_timeout(65535);
*
* // Disable touch sleep timeout
* it.set_touch_sleep_timeout(0);
* ``` * ```
* *
* After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up * Related Nextion instruction: `thsp=<value>`
* `thup`. *
* @see set_auto_wake_on_touch() Configure automatic wake on touch
* @see sleep() Manually control sleep state
*/ */
void set_touch_sleep_timeout(uint16_t touch_sleep_timeout); void set_touch_sleep_timeout(uint16_t touch_sleep_timeout = 0);
/** /**
* Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
@ -1236,20 +1242,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/ */
void set_auto_wake_on_touch(bool auto_wake_on_touch); void set_auto_wake_on_touch(bool auto_wake_on_touch);
/**
* Sets if Nextion should exit the active reparse mode before the "connect" command is sent
* @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command
* will be sent before requesting the connection from Nextion.
*
* Example:
* ```cpp
* it.set_exit_reparse_on_start(true);
* ```
*
* The display will be requested to leave active reparse mode before setup.
*/
void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; }
/** /**
* @brief Retrieves the number of commands pending in the Nextion command queue. * @brief Retrieves the number of commands pending in the Nextion command queue.
* *
@ -1292,7 +1284,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
* the Nextion display. A connection is considered established when: * the Nextion display. A connection is considered established when:
* *
* - The initial handshake with the display is completed successfully, or * - The initial handshake with the display is completed successfully, or
* - The handshake is skipped via skip_connection_handshake_ flag * - The handshake is skipped via USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE flag
* *
* The connection status is particularly useful when: * The connection status is particularly useful when:
* - Troubleshooting communication issues * - Troubleshooting communication issues
@ -1358,8 +1350,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#ifdef USE_NEXTION_CONF_START_UP_PAGE #ifdef USE_NEXTION_CONF_START_UP_PAGE
uint8_t start_up_page_ = 255; uint8_t start_up_page_ = 255;
#endif // USE_NEXTION_CONF_START_UP_PAGE #endif // USE_NEXTION_CONF_START_UP_PAGE
bool exit_reparse_on_start_ = false; bool auto_wake_on_touch_ = true;
bool skip_connection_handshake_ = false;
/** /**
* Manually send a raw command to the display and don't wait for an acknowledgement packet. * Manually send a raw command to the display and don't wait for an acknowledgement packet.
@ -1466,10 +1457,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
optional<nextion_writer_t> writer_; optional<nextion_writer_t> writer_;
optional<float> brightness_; optional<float> brightness_;
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
std::string device_model_; std::string device_model_;
std::string firmware_version_; std::string firmware_version_;
std::string serial_number_; std::string serial_number_;
std::string flash_size_; std::string flash_size_;
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
void remove_front_no_sensors_(); void remove_front_no_sensors_();

View File

@ -15,14 +15,15 @@ void Nextion::set_wake_up_page(uint8_t wake_up_page) {
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
} }
void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) { void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) {
if (touch_sleep_timeout < 3) { // Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables)
ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)"); if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) {
return; this->touch_sleep_timeout_ = 3; // Auto-correct to minimum valid value
} else {
this->touch_sleep_timeout_ = touch_sleep_timeout;
} }
this->touch_sleep_timeout_ = touch_sleep_timeout; this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true);
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true);
} }
void Nextion::sleep(bool sleep) { void Nextion::sleep(bool sleep) {

View File

@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) {
return; // This is a variable. no need to set color return; // This is a variable. no need to set color
} }
this->bco_ = bco; this->bco_ = bco;
this->bco_needs_update_ = true; this->component_flags_.bco_needs_update = true;
this->bco_is_set_ = true; this->component_flags_.bco_is_set = true;
this->update_component_settings(); this->update_component_settings();
} }
@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) {
} }
this->bco2_ = bco2; this->bco2_ = bco2;
this->bco2_needs_update_ = true; this->component_flags_.bco2_needs_update = true;
this->bco2_is_set_ = true; this->component_flags_.bco2_is_set = true;
this->update_component_settings(); this->update_component_settings();
} }
@ -29,8 +29,8 @@ void NextionComponent::set_foreground_color(Color pco) {
return; // This is a variable. no need to set color return; // This is a variable. no need to set color
} }
this->pco_ = pco; this->pco_ = pco;
this->pco_needs_update_ = true; this->component_flags_.pco_needs_update = true;
this->pco_is_set_ = true; this->component_flags_.pco_is_set = true;
this->update_component_settings(); this->update_component_settings();
} }
@ -39,8 +39,8 @@ void NextionComponent::set_foreground_pressed_color(Color pco2) {
return; // This is a variable. no need to set color return; // This is a variable. no need to set color
} }
this->pco2_ = pco2; this->pco2_ = pco2;
this->pco2_needs_update_ = true; this->component_flags_.pco2_needs_update = true;
this->pco2_is_set_ = true; this->component_flags_.pco2_is_set = true;
this->update_component_settings(); this->update_component_settings();
} }
@ -49,8 +49,8 @@ void NextionComponent::set_font_id(uint8_t font_id) {
return; // This is a variable. no need to set color return; // This is a variable. no need to set color
} }
this->font_id_ = font_id; this->font_id_ = font_id;
this->font_id_needs_update_ = true; this->component_flags_.font_id_needs_update = true;
this->font_id_is_set_ = true; this->component_flags_.font_id_is_set = true;
this->update_component_settings(); this->update_component_settings();
} }
@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) {
if (this->variable_name_ == this->variable_name_to_send_) { if (this->variable_name_ == this->variable_name_to_send_) {
return; // This is a variable. no need to set color return; // This is a variable. no need to set color
} }
this->visible_ = visible; this->component_flags_.visible = visible;
this->visible_needs_update_ = true; this->component_flags_.visible_needs_update = true;
this->visible_is_set_ = true; this->component_flags_.visible_is_set = true;
this->update_component_settings(); this->update_component_settings();
} }
void NextionComponent::update_component_settings(bool force_update) { void NextionComponent::update_component_settings(bool force_update) {
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ || if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set ||
(!this->visible_needs_update_ && !this->visible_)) { (!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) {
this->needs_to_send_update_ = true; this->needs_to_send_update_ = true;
return; return;
} }
if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) { if (this->component_flags_.visible_needs_update || (force_update && this->component_flags_.visible_is_set)) {
std::string name_to_send = this->variable_name_; std::string name_to_send = this->variable_name_;
size_t pos = name_to_send.find_last_of('.'); size_t pos = name_to_send.find_last_of('.');
@ -79,9 +79,9 @@ void NextionComponent::update_component_settings(bool force_update) {
name_to_send = name_to_send.substr(pos + 1); name_to_send = name_to_send.substr(pos + 1);
} }
this->visible_needs_update_ = false; this->component_flags_.visible_needs_update = false;
if (this->visible_) { if (this->component_flags_.visible) {
this->nextion_->show_component(name_to_send.c_str()); this->nextion_->show_component(name_to_send.c_str());
this->send_state_to_nextion(); this->send_state_to_nextion();
} else { } else {
@ -90,26 +90,26 @@ void NextionComponent::update_component_settings(bool force_update) {
} }
} }
if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) { if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_);
this->bco_needs_update_ = false; this->component_flags_.bco_needs_update = false;
} }
if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) { if (this->component_flags_.bco2_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_); this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_);
this->bco2_needs_update_ = false; this->component_flags_.bco2_needs_update = false;
} }
if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { if (this->component_flags_.pco_needs_update || (force_update && this->component_flags_.pco_is_set)) {
this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_); this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_);
this->pco_needs_update_ = false; this->component_flags_.pco_needs_update = false;
} }
if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { if (this->component_flags_.pco2_needs_update || (force_update && this->component_flags_.pco2_is_set)) {
this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_); this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_);
this->pco2_needs_update_ = false; this->component_flags_.pco2_needs_update = false;
} }
if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) { if (this->component_flags_.font_id_needs_update || (force_update && this->component_flags_.font_id_is_set)) {
this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_); this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_);
this->font_id_needs_update_ = false; this->component_flags_.font_id_needs_update = false;
} }
} }
} // namespace nextion } // namespace nextion

View File

@ -21,29 +21,64 @@ class NextionComponent : public NextionComponentBase {
void set_visible(bool visible); void set_visible(bool visible);
protected: protected:
/**
* @brief Constructor initializes component state with visible=true (default state)
*/
NextionComponent() {
component_flags_ = {}; // Zero-initialize all state
component_flags_.visible = 1; // Set default visibility to true
}
NextionBase *nextion_; NextionBase *nextion_;
bool bco_needs_update_ = false; // Color and styling properties
bool bco_is_set_ = false; Color bco_; // Background color
Color bco_; Color bco2_; // Pressed background color
bool bco2_needs_update_ = false; Color pco_; // Foreground color
bool bco2_is_set_ = false; Color pco2_; // Pressed foreground color
Color bco2_;
bool pco_needs_update_ = false;
bool pco_is_set_ = false;
Color pco_;
bool pco2_needs_update_ = false;
bool pco2_is_set_ = false;
Color pco2_;
uint8_t font_id_ = 0; uint8_t font_id_ = 0;
bool font_id_needs_update_ = false;
bool font_id_is_set_ = false;
bool visible_ = true; /**
bool visible_needs_update_ = false; * @brief Component state management using compact bitfield structure
bool visible_is_set_ = false; *
* Stores all component state flags and properties in a single 16-bit bitfield
* for efficient memory usage and improved cache locality.
*
* Each component property maintains two state flags:
* - needs_update: Indicates the property requires synchronization with the display
* - is_set: Tracks whether the property has been explicitly configured
*
* The visible field stores both the update flags and the actual visibility state.
*/
struct ComponentState {
// Background color flags
uint16_t bco_needs_update : 1;
uint16_t bco_is_set : 1;
// void send_state_to_nextion() = 0; // Pressed background color flags
uint16_t bco2_needs_update : 1;
uint16_t bco2_is_set : 1;
// Foreground color flags
uint16_t pco_needs_update : 1;
uint16_t pco_is_set : 1;
// Pressed foreground color flags
uint16_t pco2_needs_update : 1;
uint16_t pco2_is_set : 1;
// Font ID flags
uint16_t font_id_needs_update : 1;
uint16_t font_id_is_set : 1;
// Visibility flags
uint16_t visible_needs_update : 1;
uint16_t visible_is_set : 1;
uint16_t visible : 1; // Actual visibility state
// Reserved bits for future expansion
uint16_t reserved : 3;
} component_flags_;
}; };
} // namespace nextion } // namespace nextion
} // namespace esphome } // namespace esphome

View File

@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
if (this->wave_chan_id_ == UINT8_MAX) { if (this->wave_chan_id_ == UINT8_MAX) {
if (send_to_nextion) { if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) { if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true; this->needs_to_send_update_ = true;
} else { } else {
this->needs_to_send_update_ = false; this->needs_to_send_update_ = false;

View File

@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) {
return; return;
if (send_to_nextion) { if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) { if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true; this->needs_to_send_update_ = true;
} else { } else {
this->needs_to_send_update_ = false; this->needs_to_send_update_ = false;

View File

@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
return; return;
if (send_to_nextion) { if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) { if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true; this->needs_to_send_update_ = true;
} else { } else {
this->nextion_->add_no_result_to_queue_with_set(this, state); this->nextion_->add_no_result_to_queue_with_set(this, state);

View File

@ -1,5 +1,6 @@
#include "nfc.h" #include "nfc.h"
#include <cstdio> #include <cstdio>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@ -7,29 +8,9 @@ namespace nfc {
static const char *const TAG = "nfc"; static const char *const TAG = "nfc";
std::string format_uid(std::vector<uint8_t> &uid) { std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); }
char buf[(uid.size() * 2) + uid.size() - 1];
int offset = 0;
for (size_t i = 0; i < uid.size(); i++) {
const char *format = "%02X";
if (i + 1 < uid.size())
format = "%02X-";
offset += sprintf(buf + offset, format, uid[i]);
}
return std::string(buf);
}
std::string format_bytes(std::vector<uint8_t> &bytes) { std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); }
char buf[(bytes.size() * 2) + bytes.size() - 1];
int offset = 0;
for (size_t i = 0; i < bytes.size(); i++) {
const char *format = "%02X";
if (i + 1 < bytes.size())
format = "%02X ";
offset += sprintf(buf + offset, format, bytes[i]);
}
return std::string(buf);
}
uint8_t guess_tag_type(uint8_t uid_length) { uint8_t guess_tag_type(uint8_t uid_length) {
if (uid_length == 4) { if (uid_length == 4) {

View File

@ -2,8 +2,8 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "ndef_record.h"
#include "ndef_message.h" #include "ndef_message.h"
#include "ndef_record.h"
#include "nfc_tag.h" #include "nfc_tag.h"
#include <vector> #include <vector>
@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
std::string format_uid(std::vector<uint8_t> &uid); std::string format_uid(const std::vector<uint8_t> &uid);
std::string format_bytes(std::vector<uint8_t> &bytes); std::string format_bytes(const std::vector<uint8_t> &bytes);
uint8_t guess_tag_type(uint8_t uid_length); uint8_t guess_tag_type(uint8_t uid_length);
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);

View File

@ -1,5 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ESPHOME, CONF_ESPHOME,
@ -7,6 +8,7 @@ from esphome.const import (
CONF_OTA, CONF_OTA,
CONF_PLATFORM, CONF_PLATFORM,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
PlatformFramework,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -120,3 +122,18 @@ async def ota_to_code(var, config):
use_state_callback = True use_state_callback = True
if use_state_callback: if use_state_callback:
cg.add_define("USE_OTA_STATE_CALLBACK") cg.add_define("USE_OTA_STATE_CALLBACK")
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"ota_backend_arduino_esp32.cpp": {PlatformFramework.ESP32_ARDUINO},
"ota_backend_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"ota_backend_arduino_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -1,6 +1,7 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base from esphome.components import esp32, esp32_rmt, remote_base
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
@ -15,6 +16,7 @@ from esphome.const import (
CONF_TYPE, CONF_TYPE,
CONF_USE_DMA, CONF_USE_DMA,
CONF_VALUE, CONF_VALUE,
PlatformFramework,
) )
from esphome.core import CORE, TimePeriod from esphome.core import CORE, TimePeriod
@ -170,3 +172,19 @@ async def to_code(config):
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
cg.add(var.set_filter_us(config[CONF_FILTER])) cg.add(var.set_filter_us(config[CONF_FILTER]))
cg.add(var.set_idle_us(config[CONF_IDLE])) cg.add(var.set_idle_us(config[CONF_IDLE]))
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"remote_receiver_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"remote_receiver_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -1,6 +1,7 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base from esphome.components import esp32, esp32_rmt, remote_base
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CARRIER_DUTY_PERCENT, CONF_CARRIER_DUTY_PERCENT,
@ -12,6 +13,7 @@ from esphome.const import (
CONF_PIN, CONF_PIN,
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
CONF_USE_DMA, CONF_USE_DMA,
PlatformFramework,
) )
from esphome.core import CORE from esphome.core import CORE
@ -95,3 +97,19 @@ async def to_code(config):
await automation.build_automation( await automation.build_automation(
var.get_complete_trigger(), [], on_complete_config var.get_complete_trigger(), [], on_complete_config
) )
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"remote_transmitter_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"remote_transmitter_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -0,0 +1,55 @@
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_RP2040
#include "esphome/core/hal.h"
#if defined(USE_WIFI)
#include <WiFi.h>
#endif
#include <hardware/structs/rosc.h>
#include <hardware/sync.h>
namespace esphome {
uint32_t random_uint32() {
uint32_t result = 0;
for (uint8_t i = 0; i < 32; i++) {
result <<= 1;
result |= rosc_hw->randombit;
}
return result;
}
bool random_bytes(uint8_t *data, size_t len) {
while (len-- != 0) {
uint8_t result = 0;
for (uint8_t i = 0; i < 8; i++) {
result <<= 1;
result |= rosc_hw->randombit;
}
*data++ = result;
}
return true;
}
// RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
Mutex::Mutex() {}
Mutex::~Mutex() {}
void Mutex::lock() {}
bool Mutex::try_lock() { return true; }
void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#ifdef USE_WIFI
WiFi.macAddress(mac);
#endif
}
} // namespace esphome
#endif // USE_RP2040

View File

@ -118,7 +118,7 @@ optional<float> QuantileFilter::new_value(float value) {
size_t queue_size = quantile_queue.size(); size_t queue_size = quantile_queue.size();
if (queue_size) { if (queue_size) {
size_t position = ceilf(queue_size * this->quantile_) - 1; size_t position = ceilf(queue_size * this->quantile_) - 1;
ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position + 1, queue_size); ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size);
result = quantile_queue[position]; result = quantile_queue[position];
} }
} }

View File

@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@ -40,3 +41,18 @@ async def to_code(config):
elif impl == IMPLEMENTATION_BSD_SOCKETS: elif impl == IMPLEMENTATION_BSD_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
cg.add_define("USE_SOCKET_SELECT_SUPPORT") cg.add_define("USE_SOCKET_SELECT_SUPPORT")
def FILTER_SOURCE_FILES() -> list[str]:
"""Return list of socket implementation files that aren't selected by the user."""
impl = CORE.config["socket"][CONF_IMPLEMENTATION]
# Build list of files to exclude based on selected implementation
excluded = []
if impl != IMPLEMENTATION_LWIP_TCP:
excluded.append("lwip_raw_tcp_impl.cpp")
if impl != IMPLEMENTATION_BSD_SOCKETS:
excluded.append("bsd_sockets_impl.cpp")
if impl != IMPLEMENTATION_LWIP_SOCKETS:
excluded.append("lwip_sockets_impl.cpp")
return excluded

View File

@ -13,6 +13,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CLK_PIN, CONF_CLK_PIN,
@ -31,6 +32,7 @@ from esphome.const import (
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
PlatformFramework,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
@ -423,3 +425,18 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso:
{cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)}, {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)},
extra=cv.ALLOW_EXTRA, extra=cv.ALLOW_EXTRA,
) )
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"spi_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
}
)

View File

@ -224,7 +224,7 @@ bool SSD1306::is_sh1106_() const {
} }
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; } bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
bool SSD1306::is_ssd1305_() const { bool SSD1306::is_ssd1305_() const {
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_32;
} }
void SSD1306::update() { void SSD1306::update() {
this->do_update_(); this->do_update_();

View File

@ -0,0 +1,317 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import TimePeriod
MULTI_CONF = True
CODEOWNERS = ["@swoboda1337"]
DEPENDENCIES = ["spi"]
CONF_SX126X_ID = "sx126x_id"
CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate"
CONF_CODING_RATE = "coding_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_DEVIATION = "deviation"
CONF_DIO1_PIN = "dio1_pin"
CONF_HW_VERSION = "hw_version"
CONF_MODULATION = "modulation"
CONF_ON_PACKET = "on_packet"
CONF_PA_POWER = "pa_power"
CONF_PA_RAMP = "pa_ramp"
CONF_PAYLOAD_LENGTH = "payload_length"
CONF_PREAMBLE_DETECT = "preamble_detect"
CONF_PREAMBLE_SIZE = "preamble_size"
CONF_RST_PIN = "rst_pin"
CONF_RX_START = "rx_start"
CONF_RF_SWITCH = "rf_switch"
CONF_SHAPING = "shaping"
CONF_SPREADING_FACTOR = "spreading_factor"
CONF_SYNC_VALUE = "sync_value"
CONF_TCXO_VOLTAGE = "tcxo_voltage"
CONF_TCXO_DELAY = "tcxo_delay"
sx126x_ns = cg.esphome_ns.namespace("sx126x")
SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice)
SX126xListener = sx126x_ns.class_("SX126xListener")
SX126xBw = sx126x_ns.enum("SX126xBw")
SX126xPacketType = sx126x_ns.enum("SX126xPacketType")
SX126xTcxoCtrl = sx126x_ns.enum("SX126xTcxoCtrl")
SX126xRampTime = sx126x_ns.enum("SX126xRampTime")
SX126xPulseShape = sx126x_ns.enum("SX126xPulseShape")
SX126xLoraCr = sx126x_ns.enum("SX126xLoraCr")
BW = {
"4_8kHz": SX126xBw.SX126X_BW_4800,
"5_8kHz": SX126xBw.SX126X_BW_5800,
"7_3kHz": SX126xBw.SX126X_BW_7300,
"9_7kHz": SX126xBw.SX126X_BW_9700,
"11_7kHz": SX126xBw.SX126X_BW_11700,
"14_6kHz": SX126xBw.SX126X_BW_14600,
"19_5kHz": SX126xBw.SX126X_BW_19500,
"23_4kHz": SX126xBw.SX126X_BW_23400,
"29_3kHz": SX126xBw.SX126X_BW_29300,
"39_0kHz": SX126xBw.SX126X_BW_39000,
"46_9kHz": SX126xBw.SX126X_BW_46900,
"58_6kHz": SX126xBw.SX126X_BW_58600,
"78_2kHz": SX126xBw.SX126X_BW_78200,
"93_8kHz": SX126xBw.SX126X_BW_93800,
"117_3kHz": SX126xBw.SX126X_BW_117300,
"156_2kHz": SX126xBw.SX126X_BW_156200,
"187_2kHz": SX126xBw.SX126X_BW_187200,
"234_3kHz": SX126xBw.SX126X_BW_234300,
"312_0kHz": SX126xBw.SX126X_BW_312000,
"373_6kHz": SX126xBw.SX126X_BW_373600,
"467_0kHz": SX126xBw.SX126X_BW_467000,
"7_8kHz": SX126xBw.SX126X_BW_7810,
"10_4kHz": SX126xBw.SX126X_BW_10420,
"15_6kHz": SX126xBw.SX126X_BW_15630,
"20_8kHz": SX126xBw.SX126X_BW_20830,
"31_3kHz": SX126xBw.SX126X_BW_31250,
"41_7kHz": SX126xBw.SX126X_BW_41670,
"62_5kHz": SX126xBw.SX126X_BW_62500,
"125_0kHz": SX126xBw.SX126X_BW_125000,
"250_0kHz": SX126xBw.SX126X_BW_250000,
"500_0kHz": SX126xBw.SX126X_BW_500000,
}
CODING_RATE = {
"CR_4_5": SX126xLoraCr.LORA_CR_4_5,
"CR_4_6": SX126xLoraCr.LORA_CR_4_6,
"CR_4_7": SX126xLoraCr.LORA_CR_4_7,
"CR_4_8": SX126xLoraCr.LORA_CR_4_8,
}
MOD = {
"LORA": SX126xPacketType.PACKET_TYPE_LORA,
"FSK": SX126xPacketType.PACKET_TYPE_GFSK,
}
TCXO_VOLTAGE = {
"1_6V": SX126xTcxoCtrl.TCXO_CTRL_1_6V,
"1_7V": SX126xTcxoCtrl.TCXO_CTRL_1_7V,
"1_8V": SX126xTcxoCtrl.TCXO_CTRL_1_8V,
"2_2V": SX126xTcxoCtrl.TCXO_CTRL_2_2V,
"2_4V": SX126xTcxoCtrl.TCXO_CTRL_2_4V,
"2_7V": SX126xTcxoCtrl.TCXO_CTRL_2_7V,
"3_0V": SX126xTcxoCtrl.TCXO_CTRL_3_0V,
"3_3V": SX126xTcxoCtrl.TCXO_CTRL_3_3V,
"NONE": SX126xTcxoCtrl.TCXO_CTRL_NONE,
}
RAMP = {
"10us": SX126xRampTime.PA_RAMP_10,
"20us": SX126xRampTime.PA_RAMP_20,
"40us": SX126xRampTime.PA_RAMP_40,
"80us": SX126xRampTime.PA_RAMP_80,
"200us": SX126xRampTime.PA_RAMP_200,
"800us": SX126xRampTime.PA_RAMP_800,
"1700us": SX126xRampTime.PA_RAMP_1700,
"3400us": SX126xRampTime.PA_RAMP_3400,
}
SHAPING = {
"GAUSSIAN_BT_0_3": SX126xPulseShape.GAUSSIAN_BT_0_3,
"GAUSSIAN_BT_0_5": SX126xPulseShape.GAUSSIAN_BT_0_5,
"GAUSSIAN_BT_0_7": SX126xPulseShape.GAUSSIAN_BT_0_7,
"GAUSSIAN_BT_1_0": SX126xPulseShape.GAUSSIAN_BT_1_0,
"NONE": SX126xPulseShape.NO_FILTER,
}
RunImageCalAction = sx126x_ns.class_(
"RunImageCalAction", automation.Action, cg.Parented.template(SX126x)
)
SendPacketAction = sx126x_ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeTxAction = sx126x_ns.class_(
"SetModeTxAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeRxAction = sx126x_ns.class_(
"SetModeRxAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeSleepAction = sx126x_ns.class_(
"SetModeSleepAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeStandbyAction = sx126x_ns.class_(
"SetModeStandbyAction", automation.Action, cg.Parented.template(SX126x)
)
def validate_raw_data(value):
if isinstance(value, str):
return value.encode("utf-8")
if isinstance(value, list):
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
"data must either be a string wrapped in quotes or a list of bytes"
)
def validate_config(config):
lora_bws = [
"7_8kHz",
"10_4kHz",
"15_6kHz",
"20_8kHz",
"31_3kHz",
"41_7kHz",
"62_5kHz",
"125_0kHz",
"250_0kHz",
"500_0kHz",
]
if config[CONF_MODULATION] == "LORA":
if config[CONF_BANDWIDTH] not in lora_bws:
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
if config[CONF_PREAMBLE_SIZE] > 0 and config[CONF_PREAMBLE_SIZE] < 6:
raise cv.Invalid("Minimum preamble size is 6 with LORA")
if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0:
raise cv.Invalid("Payload length must be set when spreading factor is 6")
else:
if config[CONF_BANDWIDTH] in lora_bws:
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with FSK")
if config[CONF_PREAMBLE_DETECT] > len(config[CONF_SYNC_VALUE]):
raise cv.Invalid("Preamble detection length must be <= sync value length")
return config
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SX126x),
cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000),
cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22),
cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP),
cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4),
cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535),
cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_RX_START, default=True): cv.boolean,
cv.Required(CONF_RF_SWITCH): cv.boolean,
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE),
cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All(
cv.positive_time_period_microseconds,
cv.Range(max=TimePeriod(microseconds=262144000)),
),
},
)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema(True, 8e6, "mode0"))
.add_extra(validate_config)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
if CONF_ON_PACKET in config:
await automation.build_automation(
var.get_packet_trigger(),
[
(cg.std_vector.template(cg.uint8), "x"),
(cg.float_, "rssi"),
(cg.float_, "snr"),
],
config[CONF_ON_PACKET],
)
if CONF_DIO1_PIN in config:
dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN])
cg.add(var.set_dio1_pin(dio1_pin))
rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN])
cg.add(var.set_rst_pin(rst_pin))
busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
cg.add(var.set_busy_pin(busy_pin))
cg.add(var.set_bandwidth(config[CONF_BANDWIDTH]))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
cg.add(var.set_hw_version(config[CONF_HW_VERSION]))
cg.add(var.set_deviation(config[CONF_DEVIATION]))
cg.add(var.set_modulation(config[CONF_MODULATION]))
cg.add(var.set_pa_ramp(config[CONF_PA_RAMP]))
cg.add(var.set_pa_power(config[CONF_PA_POWER]))
cg.add(var.set_shaping(config[CONF_SHAPING]))
cg.add(var.set_bitrate(config[CONF_BITRATE]))
cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE]))
cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH]))
cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE]))
cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT]))
cg.add(var.set_coding_rate(config[CONF_CODING_RATE]))
cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR]))
cg.add(var.set_sync_value(config[CONF_SYNC_VALUE]))
cg.add(var.set_rx_start(config[CONF_RX_START]))
cg.add(var.set_rf_switch(config[CONF_RF_SWITCH]))
cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE]))
cg.add(var.set_tcxo_delay(config[CONF_TCXO_DELAY]))
NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(SX126x),
}
)
@automation.register_action(
"sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA
)
async def no_args_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SX126x),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
)
@automation.register_action(
"sx126x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
)
async def send_packet_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_DATA]
if isinstance(data, bytes):
data = list(data)
if cg.is_template(data):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
cg.add(var.set_data_static(data))
return var

View File

@ -0,0 +1,62 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sx126x/sx126x.h"
namespace esphome {
namespace sx126x {
template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->run_image_cal(); }
};
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX126x> {
public:
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(Ts... x) override {
if (this->static_) {
this->parent_->transmit_packet(this->data_static_);
} else {
this->parent_->transmit_packet(this->data_func_(x...));
}
}
protected:
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_tx(); }
};
template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_rx(); }
};
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_sleep(); }
};
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); }
};
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,26 @@
import esphome.codegen as cg
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
import esphome.config_validation as cv
from esphome.cpp_types import PollingComponent
from .. import CONF_SX126X_ID, SX126x, SX126xListener, sx126x_ns
SX126xTransport = sx126x_ns.class_(
"SX126xTransport", PacketTransport, PollingComponent, SX126xListener
)
CONFIG_SCHEMA = transport_schema(SX126xTransport).extend(
{
cv.GenerateID(CONF_SX126X_ID): cv.use_id(SX126x),
}
)
async def to_code(config):
var, _ = await new_packet_transport(config)
sx126x = await cg.get_variable(config[CONF_SX126X_ID])
cg.add(var.set_parent(sx126x))

View File

@ -0,0 +1,26 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "sx126x_transport.h"
namespace esphome {
namespace sx126x {
static const char *const TAG = "sx126x_transport";
void SX126xTransport::setup() {
PacketTransport::setup();
this->parent_->register_listener(this);
}
void SX126xTransport::update() {
PacketTransport::update();
this->updated_ = true;
this->resend_data_ = true;
}
void SX126xTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->transmit_packet(buf); }
void SX126xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sx126x/sx126x.h"
#include "esphome/components/packet_transport/packet_transport.h"
#include <vector>
namespace esphome {
namespace sx126x {
class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener {
public:
void setup() override;
void update() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; }
size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); }
};
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,523 @@
#include "sx126x.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sx126x {
static const char *const TAG = "sx126x";
static const uint16_t RAMP[8] = {10, 20, 40, 80, 200, 800, 1700, 3400};
static const uint32_t BW_HZ[31] = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400, 29300, 39000, 46900,
58600, 78200, 93800, 117300, 156200, 187200, 234300, 312000, 373600, 467000, 7810,
10420, 15630, 20830, 31250, 41670, 62500, 125000, 250000, 500000};
static const uint8_t BW_LORA[10] = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830, LORA_BW_31250,
LORA_BW_41670, LORA_BW_62500, LORA_BW_125000, LORA_BW_250000, LORA_BW_500000};
static const uint8_t BW_FSK[21] = {
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
static constexpr uint32_t SWITCHING_DELAY_US = 1;
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
// OCP (Over Current Protection) values
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
// LoRa low data rate optimization threshold
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
uint8_t SX126x::read_fifo_(uint8_t offset, std::vector<uint8_t> &packet) {
this->wait_busy_();
this->enable();
this->transfer_byte(RADIO_READ_BUFFER);
this->transfer_byte(offset);
uint8_t status = this->transfer_byte(0x00);
for (uint8_t &byte : packet) {
byte = this->transfer_byte(0x00);
}
this->disable();
return status;
}
void SX126x::write_fifo_(uint8_t offset, const std::vector<uint8_t> &packet) {
this->wait_busy_();
this->enable();
this->transfer_byte(RADIO_WRITE_BUFFER);
this->transfer_byte(offset);
for (const uint8_t &byte : packet) {
this->transfer_byte(byte);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->transfer_byte(opcode);
uint8_t status = this->transfer_byte(0x00);
for (int32_t i = 0; i < size; i++) {
data[i] = this->transfer_byte(0x00);
}
this->disable();
return status;
}
void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->transfer_byte(opcode);
for (int32_t i = 0; i < size; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->write_byte(RADIO_READ_REGISTER);
this->write_byte((reg >> 8) & 0xFF);
this->write_byte((reg >> 0) & 0xFF);
this->write_byte(0x00);
for (int32_t i = 0; i < size; i++) {
data[i] = this->transfer_byte(0x00);
}
this->disable();
}
void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->write_byte(RADIO_WRITE_REGISTER);
this->write_byte((reg >> 8) & 0xFF);
this->write_byte((reg >> 0) & 0xFF);
for (int32_t i = 0; i < size; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
void SX126x::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
// setup pins
this->busy_pin_->setup();
this->rst_pin_->setup();
this->dio1_pin_->setup();
// start spi
this->spi_setup();
// configure rf
this->configure();
}
void SX126x::configure() {
uint8_t buf[8];
// toggle chip reset
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
this->rst_pin_->digital_write(false);
delayMicroseconds(RESET_DELAY_LOW_US);
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
// wakeup
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
// config tcxo
if (this->tcxo_voltage_ != TCXO_CTRL_NONE) {
uint32_t delay = this->tcxo_delay_ >> 6;
buf[0] = this->tcxo_voltage_;
buf[1] = (delay >> 16) & 0xFF;
buf[2] = (delay >> 8) & 0xFF;
buf[3] = (delay >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
buf[0] = 0x7F;
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
}
// clear errors
buf[0] = 0x00;
buf[1] = 0x00;
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
// rf switch
if (this->rf_switch_) {
buf[0] = 0x01;
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
}
// check silicon version to make sure hw is ok
this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16);
if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) {
this->mark_failed();
return;
}
// setup packet type
buf[0] = this->modulation_;
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
// calibrate image
this->run_image_cal();
// set frequency
uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ;
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
buf[3] = (uint8_t) (freq & 0xFF);
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
// configure pa
int8_t pa_power = this->pa_power_;
if (this->hw_version_ == "sx1261") {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
if (pa_power == 15) {
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
} else {
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
}
pa_power = std::max(pa_power, (int8_t) -3);
pa_power = std::min(pa_power, (int8_t) 14);
buf[0] = OCP_80MA;
this->write_register_(REG_OCP, buf, 1);
} else {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
pa_power = std::max(pa_power, (int8_t) -3);
pa_power = std::min(pa_power, (int8_t) 22);
buf[0] = OCP_140MA;
this->write_register_(REG_OCP, buf, 1);
}
buf[0] = pa_power;
buf[1] = this->pa_ramp_;
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
// configure modem
if (this->modulation_ == PACKET_TYPE_LORA) {
// set modulation params
float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_];
buf[0] = this->spreading_factor_;
buf[1] = BW_LORA[this->bandwidth_ - SX126X_BW_7810];
buf[2] = this->coding_rate_;
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
// set packet params and sync word
this->set_packet_params_(this->payload_length_);
if (this->sync_value_.size() == 2) {
this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
}
} else {
// set modulation params
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_;
uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ;
buf[0] = (bitrate >> 16) & 0xFF;
buf[1] = (bitrate >> 8) & 0xFF;
buf[2] = (bitrate >> 0) & 0xFF;
buf[3] = this->shaping_;
buf[4] = BW_FSK[this->bandwidth_ - SX126X_BW_4800];
buf[5] = (fdev >> 16) & 0xFF;
buf[6] = (fdev >> 8) & 0xFF;
buf[7] = (fdev >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
// set packet params and sync word
this->set_packet_params_(this->payload_length_);
if (!this->sync_value_.empty()) {
this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
}
}
// switch to rx or sleep
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
}
size_t SX126x::get_max_packet_size() {
if (this->payload_length_ > 0) {
return this->payload_length_;
}
return 255;
}
void SX126x::set_packet_params_(uint8_t payload_length) {
uint8_t buf[9];
if (this->modulation_ == PACKET_TYPE_LORA) {
buf[0] = (this->preamble_size_ >> 8) & 0xFF;
buf[1] = (this->preamble_size_ >> 0) & 0xFF;
buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00;
buf[3] = payload_length;
buf[4] = (this->crc_enable_) ? 0x01 : 0x00;
buf[5] = 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6);
} else {
uint16_t preamble_size = this->preamble_size_ * 8;
buf[0] = (preamble_size >> 8) & 0xFF;
buf[1] = (preamble_size >> 0) & 0xFF;
buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
buf[3] = this->sync_value_.size() * 8;
buf[4] = 0x00;
buf[5] = 0x00;
buf[6] = payload_length;
buf[7] = this->crc_enable_ ? 0x06 : 0x01;
buf[8] = 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
}
}
SX126xError SX126x::transmit_packet(const std::vector<uint8_t> &packet) {
if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX126xError::INVALID_PARAMS;
}
if (packet.empty() || packet.size() > this->get_max_packet_size()) {
ESP_LOGE(TAG, "Packet size out of range");
return SX126xError::INVALID_PARAMS;
}
SX126xError ret = SX126xError::NONE;
this->set_mode_standby(STDBY_XOSC);
if (this->payload_length_ == 0) {
this->set_packet_params_(packet.size());
}
this->write_fifo_(0x00, packet);
this->set_mode_tx();
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio1_pin_->digital_read()) {
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX126xError::TIMEOUT;
break;
}
}
uint8_t buf[2];
buf[0] = 0xFF;
buf[1] = 0xFF;
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
return ret;
}
void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, rssi, snr);
}
this->packet_trigger_->trigger(packet, rssi, snr);
}
void SX126x::loop() {
if (!this->dio1_pin_->digital_read()) {
return;
}
uint16_t status;
uint8_t buf[3];
uint8_t rssi;
int8_t snr;
this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2);
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
status = (buf[0] << 8) | buf[1];
if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) {
if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) {
this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3);
if (this->modulation_ == PACKET_TYPE_LORA) {
rssi = buf[0];
snr = buf[1];
} else {
rssi = buf[2];
snr = 0;
}
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2);
this->packet_.resize(buf[0]);
this->read_fifo_(buf[1], this->packet_);
this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f);
}
}
}
void SX126x::run_image_cal() {
// the following values were taken from section 9.2.1 table 9-2
// in rev 2.1 of the datasheet
uint8_t buf[2] = {0, 0};
if (this->frequency_ > 900000000) {
buf[0] = 0xE1;
buf[1] = 0xE9;
} else if (this->frequency_ > 850000000) {
buf[0] = 0xD7;
buf[1] = 0xD8;
} else if (this->frequency_ > 770000000) {
buf[0] = 0xC1;
buf[1] = 0xC5;
} else if (this->frequency_ > 460000000) {
buf[0] = 0x75;
buf[1] = 0x81;
} else if (this->frequency_ > 425000000) {
buf[0] = 0x6B;
buf[1] = 0x6F;
}
if (buf[0] > 0 && buf[1] > 0) {
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
}
}
void SX126x::set_mode_rx() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// set timeout to 0
buf[0] = 0x00;
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
// switch to continuous mode rx
buf[0] = 0xFF;
buf[1] = 0xFF;
buf[2] = 0xFF;
this->write_opcode_(RADIO_SET_RX, buf, 3);
}
void SX126x::set_mode_tx() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// switch to single mode tx
buf[0] = 0x00;
buf[1] = 0x00;
buf[2] = 0x00;
this->write_opcode_(RADIO_SET_TX, buf, 3);
}
void SX126x::set_mode_sleep() {
uint8_t buf[1];
buf[0] = 0x05;
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
}
void SX126x::set_mode_standby(SX126xStandbyMode mode) {
uint8_t buf[1];
buf[0] = mode;
this->write_opcode_(RADIO_SET_STANDBY, buf, 1);
}
void SX126x::wait_busy_() {
// wait if the device is busy, the maximum delay is only be a few ms
// with most commands taking only a few us
uint32_t start = millis();
while (this->busy_pin_->digital_read()) {
if (millis() - start > BUSY_TIMEOUT_MS) {
ESP_LOGE(TAG, "Wait busy timeout");
this->mark_failed();
break;
}
}
}
void SX126x::dump_config() {
ESP_LOGCONFIG(TAG, "SX126x:");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" BUSY Pin: ", this->busy_pin_);
LOG_PIN(" RST Pin: ", this->rst_pin_);
LOG_PIN(" DIO1 Pin: ", this->dio1_pin_);
ESP_LOGCONFIG(TAG,
" HW Version: %15s\n"
" Frequency: %" PRIu32 " Hz\n"
" Bandwidth: %" PRIu32 " Hz\n"
" PA Power: %" PRId8 " dBm\n"
" PA Ramp: %" PRIu16 " us\n"
" Payload Length: %" PRIu32 "\n"
" CRC Enable: %s\n"
" Rx Start: %s",
this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_, RAMP[this->pa_ramp_],
this->payload_length_, TRUEFALSE(this->crc_enable_), TRUEFALSE(this->rx_start_));
if (this->modulation_ == PACKET_TYPE_GFSK) {
const char *shaping = "NONE";
if (this->shaping_ == GAUSSIAN_BT_0_3) {
shaping = "GAUSSIAN_BT_0_3";
} else if (this->shaping_ == GAUSSIAN_BT_0_5) {
shaping = "GAUSSIAN_BT_0_5";
} else if (this->shaping_ == GAUSSIAN_BT_0_7) {
shaping = "GAUSSIAN_BT_0_7";
} else if (this->shaping_ == GAUSSIAN_BT_1_0) {
shaping = "GAUSSIAN_BT_1_0";
}
ESP_LOGCONFIG(TAG,
" Modulation: FSK\n"
" Deviation: %" PRIu32 " Hz\n"
" Shaping: %s\n"
" Preamble Size: %" PRIu16 "\n"
" Preamble Detect: %" PRIu16 "\n"
" Bitrate: %" PRIu32 "b/s",
this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_);
} else if (this->modulation_ == PACKET_TYPE_LORA) {
const char *cr = "4/8";
if (this->coding_rate_ == LORA_CR_4_5) {
cr = "4/5";
} else if (this->coding_rate_ == LORA_CR_4_6) {
cr = "4/6";
} else if (this->coding_rate_ == LORA_CR_4_7) {
cr = "4/7";
}
ESP_LOGCONFIG(TAG,
" Modulation: LORA\n"
" Spreading Factor: %" PRIu8 "\n"
" Coding Rate: %s\n"
" Preamble Size: %" PRIu16,
this->spreading_factor_, cr, this->preamble_size_);
}
if (!this->sync_value_.empty()) {
ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str());
}
if (this->is_failed()) {
ESP_LOGE(TAG, "Configuring SX126x failed");
}
}
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,140 @@
#pragma once
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "sx126x_reg.h"
#include <utility>
#include <vector>
namespace esphome {
namespace sx126x {
enum SX126xBw : uint8_t {
// FSK
SX126X_BW_4800,
SX126X_BW_5800,
SX126X_BW_7300,
SX126X_BW_9700,
SX126X_BW_11700,
SX126X_BW_14600,
SX126X_BW_19500,
SX126X_BW_23400,
SX126X_BW_29300,
SX126X_BW_39000,
SX126X_BW_46900,
SX126X_BW_58600,
SX126X_BW_78200,
SX126X_BW_93800,
SX126X_BW_117300,
SX126X_BW_156200,
SX126X_BW_187200,
SX126X_BW_234300,
SX126X_BW_312000,
SX126X_BW_373600,
SX126X_BW_467000,
// LORA
SX126X_BW_7810,
SX126X_BW_10420,
SX126X_BW_15630,
SX126X_BW_20830,
SX126X_BW_31250,
SX126X_BW_41670,
SX126X_BW_62500,
SX126X_BW_125000,
SX126X_BW_250000,
SX126X_BW_500000,
};
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
class SX126xListener {
public:
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
};
class SX126x : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
size_t get_max_packet_size();
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void setup() override;
void loop() override;
void dump_config() override;
void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; }
void set_mode_rx();
void set_mode_tx();
void set_mode_standby(SX126xStandbyMode mode);
void set_mode_sleep();
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
void set_pa_power(int8_t power) { this->pa_power_ = power; }
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->sync_value_ = sync_value; }
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; }
void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; }
void run_image_cal();
void configure();
SX126xError transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() const { return this->packet_trigger_; };
protected:
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
uint8_t read_fifo_(uint8_t offset, std::vector<uint8_t> &packet);
void write_fifo_(uint8_t offset, const std::vector<uint8_t> &packet);
void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
void write_register_(uint16_t reg, uint8_t *data, uint8_t size);
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr);
void wait_busy_();
Trigger<std::vector<uint8_t>, float, float> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, float>()};
std::vector<SX126xListener *> listeners_;
std::vector<uint8_t> packet_;
std::vector<uint8_t> sync_value_;
InternalGPIOPin *busy_pin_{nullptr};
InternalGPIOPin *dio1_pin_{nullptr};
InternalGPIOPin *rst_pin_{nullptr};
std::string hw_version_;
char version_[16];
SX126xBw bandwidth_{SX126X_BW_125000};
uint32_t bitrate_{0};
uint32_t deviation_{0};
uint32_t frequency_{0};
uint32_t payload_length_{0};
uint32_t tcxo_delay_{0};
uint16_t preamble_detect_{0};
uint16_t preamble_size_{0};
uint8_t tcxo_voltage_{0};
uint8_t coding_rate_{0};
uint8_t modulation_{PACKET_TYPE_LORA};
uint8_t pa_ramp_{0};
uint8_t shaping_{0};
uint8_t spreading_factor_{0};
int8_t pa_power_{0};
bool crc_enable_{false};
bool rx_start_{false};
bool rf_switch_{false};
};
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,163 @@
#pragma once
#include "esphome/core/hal.h"
namespace esphome {
namespace sx126x {
static const uint32_t XTAL_FREQ = 32000000;
enum SX126xOpCode : uint8_t {
RADIO_GET_STATUS = 0xC0,
RADIO_WRITE_REGISTER = 0x0D,
RADIO_READ_REGISTER = 0x1D,
RADIO_WRITE_BUFFER = 0x0E,
RADIO_READ_BUFFER = 0x1E,
RADIO_SET_SLEEP = 0x84,
RADIO_SET_STANDBY = 0x80,
RADIO_SET_FS = 0xC1,
RADIO_SET_TX = 0x83,
RADIO_SET_RX = 0x82,
RADIO_SET_RXDUTYCYCLE = 0x94,
RADIO_SET_CAD = 0xC5,
RADIO_SET_TXCONTINUOUSWAVE = 0xD1,
RADIO_SET_TXCONTINUOUSPREAMBLE = 0xD2,
RADIO_SET_PACKETTYPE = 0x8A,
RADIO_GET_PACKETTYPE = 0x11,
RADIO_SET_RFFREQUENCY = 0x86,
RADIO_SET_TXPARAMS = 0x8E,
RADIO_SET_PACONFIG = 0x95,
RADIO_SET_CADPARAMS = 0x88,
RADIO_SET_BUFFERBASEADDRESS = 0x8F,
RADIO_SET_MODULATIONPARAMS = 0x8B,
RADIO_SET_PACKETPARAMS = 0x8C,
RADIO_GET_RXBUFFERSTATUS = 0x13,
RADIO_GET_PACKETSTATUS = 0x14,
RADIO_GET_RSSIINST = 0x15,
RADIO_GET_STATS = 0x10,
RADIO_RESET_STATS = 0x00,
RADIO_SET_DIOIRQPARAMS = 0x08,
RADIO_GET_IRQSTATUS = 0x12,
RADIO_CLR_IRQSTATUS = 0x02,
RADIO_CALIBRATE = 0x89,
RADIO_CALIBRATEIMAGE = 0x98,
RADIO_SET_REGULATORMODE = 0x96,
RADIO_GET_ERROR = 0x17,
RADIO_CLR_ERROR = 0x07,
RADIO_SET_TCXOMODE = 0x97,
RADIO_SET_TXFALLBACKMODE = 0x93,
RADIO_SET_RFSWITCHMODE = 0x9D,
RADIO_SET_STOPRXTIMERONPREAMBLE = 0x9F,
RADIO_SET_LORASYMBTIMEOUT = 0xA0,
};
enum SX126xRegister : uint16_t {
REG_VERSION_STRING = 0x0320,
REG_GFSK_SYNCWORD = 0x06C0,
REG_LORA_SYNCWORD = 0x0740,
REG_OCP = 0x08E7,
};
enum SX126xStandbyMode : uint8_t {
STDBY_RC = 0x00,
STDBY_XOSC = 0x01,
};
enum SX126xPacketType : uint8_t {
PACKET_TYPE_GFSK = 0x00,
PACKET_TYPE_LORA = 0x01,
PACKET_TYPE_LRHSS = 0x03,
};
enum SX126xFskBw : uint8_t {
FSK_BW_4800 = 0x1F,
FSK_BW_5800 = 0x17,
FSK_BW_7300 = 0x0F,
FSK_BW_9700 = 0x1E,
FSK_BW_11700 = 0x16,
FSK_BW_14600 = 0x0E,
FSK_BW_19500 = 0x1D,
FSK_BW_23400 = 0x15,
FSK_BW_29300 = 0x0D,
FSK_BW_39000 = 0x1C,
FSK_BW_46900 = 0x14,
FSK_BW_58600 = 0x0C,
FSK_BW_78200 = 0x1B,
FSK_BW_93800 = 0x13,
FSK_BW_117300 = 0x0B,
FSK_BW_156200 = 0x1A,
FSK_BW_187200 = 0x12,
FSK_BW_234300 = 0x0A,
FSK_BW_312000 = 0x19,
FSK_BW_373600 = 0x11,
FSK_BW_467000 = 0x09,
};
enum SX126xLoraBw : uint8_t {
LORA_BW_7810 = 0x00,
LORA_BW_10420 = 0x08,
LORA_BW_15630 = 0x01,
LORA_BW_20830 = 0x09,
LORA_BW_31250 = 0x02,
LORA_BW_41670 = 0x0A,
LORA_BW_62500 = 0x03,
LORA_BW_125000 = 0x04,
LORA_BW_250000 = 0x05,
LORA_BW_500000 = 0x06,
};
enum SX126xLoraCr : uint8_t {
LORA_CR_4_5 = 0x01,
LORA_CR_4_6 = 0x02,
LORA_CR_4_7 = 0x03,
LORA_CR_4_8 = 0x04,
};
enum SX126xIrqMasks : uint16_t {
IRQ_RADIO_NONE = 0x0000,
IRQ_TX_DONE = 0x0001,
IRQ_RX_DONE = 0x0002,
IRQ_PREAMBLE_DETECTED = 0x0004,
IRQ_SYNCWORD_VALID = 0x0008,
IRQ_HEADER_VALID = 0x0010,
IRQ_HEADER_ERROR = 0x0020,
IRQ_CRC_ERROR = 0x0040,
IRQ_CAD_DONE = 0x0080,
IRQ_CAD_ACTIVITY_DETECTED = 0x0100,
IRQ_RX_TX_TIMEOUT = 0x0200,
IRQ_RADIO_ALL = 0xFFFF,
};
enum SX126xTcxoCtrl : uint8_t {
TCXO_CTRL_1_6V = 0x00,
TCXO_CTRL_1_7V = 0x01,
TCXO_CTRL_1_8V = 0x02,
TCXO_CTRL_2_2V = 0x03,
TCXO_CTRL_2_4V = 0x04,
TCXO_CTRL_2_7V = 0x05,
TCXO_CTRL_3_0V = 0x06,
TCXO_CTRL_3_3V = 0x07,
TCXO_CTRL_NONE = 0xFF,
};
enum SX126xPulseShape : uint8_t {
NO_FILTER = 0x00,
GAUSSIAN_BT_0_3 = 0x08,
GAUSSIAN_BT_0_5 = 0x09,
GAUSSIAN_BT_0_7 = 0x0A,
GAUSSIAN_BT_1_0 = 0x0B,
};
enum SX126xRampTime : uint8_t {
PA_RAMP_10 = 0x00,
PA_RAMP_20 = 0x01,
PA_RAMP_40 = 0x02,
PA_RAMP_80 = 0x03,
PA_RAMP_200 = 0x04,
PA_RAMP_800 = 0x05,
PA_RAMP_1700 = 0x06,
PA_RAMP_3400 = 0x07,
};
} // namespace sx126x
} // namespace esphome

View File

@ -21,10 +21,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = {
void Syslog::setup() { void Syslog::setup() {
logger::global_logger->add_on_log_callback( logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message) { this->log_(level, tag, message); }); [this](int level, const char *tag, const char *message, size_t message_len) {
this->log_(level, tag, message, message_len);
});
} }
void Syslog::log_(const int level, const char *tag, const char *message) const { void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const {
if (level > this->log_level_) if (level > this->log_level_)
return; return;
// Syslog PRI calculation: facility * 8 + severity // Syslog PRI calculation: facility * 8 + severity
@ -34,7 +36,7 @@ void Syslog::log_(const int level, const char *tag, const char *message) const {
} }
int pri = this->facility_ * 8 + severity; int pri = this->facility_ * 8 + severity;
auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S"); auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S");
unsigned len = strlen(message); size_t len = message_len;
// remove color formatting // remove color formatting
if (this->strip_ && message[0] == 0x1B && len > 11) { if (this->strip_ && message[0] == 0x1B && len > 11) {
message += 7; message += 7;

View File

@ -17,7 +17,7 @@ class Syslog : public Component, public Parented<udp::UDPComponent> {
protected: protected:
int log_level_; int log_level_;
void log_(int level, const char *tag, const char *message) const; void log_(int level, const char *tag, const char *message, size_t message_len) const;
time::RealTimeClock *time_; time::RealTimeClock *time_;
bool strip_{true}; bool strip_{true};
int facility_{16}; int facility_{16};

View File

@ -2,6 +2,7 @@ import re
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AFTER, CONF_AFTER,
@ -27,6 +28,7 @@ from esphome.const import (
CONF_TX_PIN, CONF_TX_PIN,
CONF_UART_ID, CONF_UART_ID,
PLATFORM_HOST, PLATFORM_HOST,
PlatformFramework,
) )
from esphome.core import CORE from esphome.core import CORE
import esphome.final_validate as fv import esphome.final_validate as fv
@ -438,3 +440,19 @@ async def uart_write_to_code(config, action_id, template_arg, args):
else: else:
cg.add(var.set_data_static(data)) cg.add(var.set_data_static(data))
return var return var
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
"uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"uart_component_host.cpp": {PlatformFramework.HOST_NATIVE},
"uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"uart_component_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() {
} else { } else {
// Partial out (PTOUT), makes the display exit partial mode // Partial out (PTOUT), makes the display exit partial mode
this->command(0x92); this->command(0x92);
ESP_LOGD(TAG, "Partial update done, next full update after %d cycles", ESP_LOGD(TAG, "Partial update done, next full update after %" PRIu32 " cycles",
this->full_update_every_ - this->at_update_ - 1); this->full_update_every_ - this->at_update_ - 1);
} }

View File

@ -287,7 +287,8 @@ void WebServer::setup() {
if (logger::global_logger != nullptr && this->expose_log_) { if (logger::global_logger != nullptr && this->expose_log_) {
logger::global_logger->add_on_log_callback( logger::global_logger->add_on_log_callback(
// logs are not deferred, the memory overhead would be too large // logs are not deferred, the memory overhead would be too large
[this](int level, const char *tag, const char *message) { [this](int level, const char *tag, const char *message, size_t message_len) {
(void) message_len;
this->events_.try_send_nodefer(message, "log", millis()); this->events_.try_send_nodefer(message, "log", millis());
}); });
} }

View File

@ -78,7 +78,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred
update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with
multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's
implemented in a more straightforward way for ESP-IDF. Arudino platform will eventually go away and this workaround implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround
can be forgotten. can be forgotten.
*/ */
#ifdef USE_ARDUINO #ifdef USE_ARDUINO

View File

@ -3,6 +3,7 @@ from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.network import IPAddress from esphome.components.network import IPAddress
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AP, CONF_AP,
@ -39,6 +40,7 @@ from esphome.const import (
CONF_TTLS_PHASE_2, CONF_TTLS_PHASE_2,
CONF_USE_ADDRESS, CONF_USE_ADDRESS,
CONF_USERNAME, CONF_USERNAME,
PlatformFramework,
) )
from esphome.core import CORE, HexInt, coroutine_with_priority from esphome.core import CORE, HexInt, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
@ -526,3 +528,18 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
await automation.build_automation(var.get_error_trigger(), [], on_error_config) await automation.build_automation(var.get_error_trigger(), [], on_error_config)
await cg.register_component(var, config) await cg.register_component(var, config)
return var return var
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
"wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"wifi_component_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
}
)

View File

@ -1,4 +1,20 @@
from esphome.const import CONF_ID from collections.abc import Callable
from esphome.const import (
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PlatformFramework,
)
from esphome.core import CORE
# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
_PLATFORM_FRAMEWORK_LOOKUP = {
(pf.value[0].value, pf.value[1].value): pf for pf in PlatformFramework
}
class Extend: class Extend:
@ -103,3 +119,60 @@ def merge_config(full_old, full_new):
return new return new
return merge(full_old, full_new) return merge(full_old, full_new)
def filter_source_files_from_platform(
files_map: dict[str, set[PlatformFramework]],
) -> Callable[[], list[str]]:
"""Helper to build a FILTER_SOURCE_FILES function from platform mapping.
Args:
files_map: Dict mapping filename to set of PlatformFramework enums
that should compile this file
Returns:
Function that returns list of files to exclude for current platform
"""
def filter_source_files() -> list[str]:
# Get current platform/framework
core_data = CORE.data.get(KEY_CORE, {})
target_platform = core_data.get(KEY_TARGET_PLATFORM)
target_framework = core_data.get(KEY_TARGET_FRAMEWORK)
if not target_platform or not target_framework:
return []
# Direct lookup of current PlatformFramework
current_platform_framework = _PLATFORM_FRAMEWORK_LOOKUP.get(
(target_platform, target_framework)
)
if not current_platform_framework:
return []
# Return files that should be excluded for current platform
return [
filename
for filename, platforms in files_map.items()
if current_platform_framework not in platforms
]
return filter_source_files
def get_logger_level() -> str:
"""Get the configured logger level.
This is used by components to determine what logging features to include
based on the configured log level.
Returns:
The configured logger level string, defaults to "DEBUG" if not configured
"""
# Check if logger config exists
if CONF_LOGGER not in CORE.config:
return "DEBUG"
logger_config = CORE.config[CONF_LOGGER]
return logger_config.get(CONF_LEVEL, "DEBUG")

View File

@ -1,20 +1,65 @@
"""Constants used by esphome.""" """Constants used by esphome."""
__version__ = "2025.7.0-dev" from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.8.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
) )
PLATFORM_BK72XX = "bk72xx"
PLATFORM_ESP32 = "esp32" class Platform(StrEnum):
PLATFORM_ESP8266 = "esp8266" """Platform identifiers for ESPHome."""
PLATFORM_HOST = "host"
PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" BK72XX = "bk72xx"
PLATFORM_LN882X = "ln882x" ESP32 = "esp32"
PLATFORM_RP2040 = "rp2040" ESP8266 = "esp8266"
PLATFORM_RTL87XX = "rtl87xx" HOST = "host"
LIBRETINY_OLDSTYLE = "libretiny"
LN882X = "ln882x"
RP2040 = "rp2040"
RTL87XX = "rtl87xx"
class Framework(StrEnum):
"""Framework identifiers for ESPHome."""
ARDUINO = "arduino"
ESP_IDF = "esp-idf"
NATIVE = "host"
class PlatformFramework(Enum):
"""Combined platform-framework identifiers with tuple values."""
# ESP32 variants
ESP32_ARDUINO = (Platform.ESP32, Framework.ARDUINO)
ESP32_IDF = (Platform.ESP32, Framework.ESP_IDF)
# Arduino framework platforms
ESP8266_ARDUINO = (Platform.ESP8266, Framework.ARDUINO)
RP2040_ARDUINO = (Platform.RP2040, Framework.ARDUINO)
BK72XX_ARDUINO = (Platform.BK72XX, Framework.ARDUINO)
RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
# Host platform (native)
HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
# Maintain backward compatibility by reassigning after enum definition
PLATFORM_BK72XX = Platform.BK72XX
PLATFORM_ESP32 = Platform.ESP32
PLATFORM_ESP8266 = Platform.ESP8266
PLATFORM_HOST = Platform.HOST
PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
PLATFORM_LN882X = Platform.LN882X
PLATFORM_RP2040 = Platform.RP2040
PLATFORM_RTL87XX = Platform.RTL87XX
SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}

View File

@ -368,6 +368,17 @@ class Application {
uint8_t get_app_state() const { return this->app_state_; } uint8_t get_app_state() const { return this->app_state_; }
// Helper macro for entity getter method declarations - reduces code duplication
// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
for (auto *obj : this->entities_member##_) { \
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \
return obj; \
} \
return nullptr; \
}
#ifdef USE_DEVICES #ifdef USE_DEVICES
const std::vector<Device *> &get_devices() { return this->devices_; } const std::vector<Device *> &get_devices() { return this->devices_; }
#endif #endif
@ -376,218 +387,92 @@ class Application {
#endif #endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; } const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors)
for (auto *obj : this->binary_sensors_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
const std::vector<switch_::Switch *> &get_switches() { return this->switches_; } const std::vector<switch_::Switch *> &get_switches() { return this->switches_; }
switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(switch_::Switch, switch, switches)
for (auto *obj : this->switches_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
const std::vector<button::Button *> &get_buttons() { return this->buttons_; } const std::vector<button::Button *> &get_buttons() { return this->buttons_; }
button::Button *get_button_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(button::Button, button, buttons)
for (auto *obj : this->buttons_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
const std::vector<sensor::Sensor *> &get_sensors() { return this->sensors_; } const std::vector<sensor::Sensor *> &get_sensors() { return this->sensors_; }
sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors)
for (auto *obj : this->sensors_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
const std::vector<text_sensor::TextSensor *> &get_text_sensors() { return this->text_sensors_; } const std::vector<text_sensor::TextSensor *> &get_text_sensors() { return this->text_sensors_; }
text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors)
for (auto *obj : this->text_sensors_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
const std::vector<fan::Fan *> &get_fans() { return this->fans_; } const std::vector<fan::Fan *> &get_fans() { return this->fans_; }
fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(fan::Fan, fan, fans)
for (auto *obj : this->fans_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
const std::vector<cover::Cover *> &get_covers() { return this->covers_; } const std::vector<cover::Cover *> &get_covers() { return this->covers_; }
cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(cover::Cover, cover, covers)
for (auto *obj : this->covers_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
const std::vector<light::LightState *> &get_lights() { return this->lights_; } const std::vector<light::LightState *> &get_lights() { return this->lights_; }
light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(light::LightState, light, lights)
for (auto *obj : this->lights_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
const std::vector<climate::Climate *> &get_climates() { return this->climates_; } const std::vector<climate::Climate *> &get_climates() { return this->climates_; }
climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(climate::Climate, climate, climates)
for (auto *obj : this->climates_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
const std::vector<number::Number *> &get_numbers() { return this->numbers_; } const std::vector<number::Number *> &get_numbers() { return this->numbers_; }
number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(number::Number, number, numbers)
for (auto *obj : this->numbers_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
const std::vector<datetime::DateEntity *> &get_dates() { return this->dates_; } const std::vector<datetime::DateEntity *> &get_dates() { return this->dates_; }
datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(datetime::DateEntity, date, dates)
for (auto *obj : this->dates_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
const std::vector<datetime::TimeEntity *> &get_times() { return this->times_; } const std::vector<datetime::TimeEntity *> &get_times() { return this->times_; }
datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(datetime::TimeEntity, time, times)
for (auto *obj : this->times_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
const std::vector<datetime::DateTimeEntity *> &get_datetimes() { return this->datetimes_; } const std::vector<datetime::DateTimeEntity *> &get_datetimes() { return this->datetimes_; }
datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes)
for (auto *obj : this->datetimes_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
const std::vector<text::Text *> &get_texts() { return this->texts_; } const std::vector<text::Text *> &get_texts() { return this->texts_; }
text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(text::Text, text, texts)
for (auto *obj : this->texts_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
const std::vector<select::Select *> &get_selects() { return this->selects_; } const std::vector<select::Select *> &get_selects() { return this->selects_; }
select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(select::Select, select, selects)
for (auto *obj : this->selects_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
const std::vector<lock::Lock *> &get_locks() { return this->locks_; } const std::vector<lock::Lock *> &get_locks() { return this->locks_; }
lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(lock::Lock, lock, locks)
for (auto *obj : this->locks_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
const std::vector<valve::Valve *> &get_valves() { return this->valves_; } const std::vector<valve::Valve *> &get_valves() { return this->valves_; }
valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(valve::Valve, valve, valves)
for (auto *obj : this->valves_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
const std::vector<media_player::MediaPlayer *> &get_media_players() { return this->media_players_; } const std::vector<media_player::MediaPlayer *> &get_media_players() { return this->media_players_; }
media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players)
for (auto *obj : this->media_players_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
const std::vector<alarm_control_panel::AlarmControlPanel *> &get_alarm_control_panels() { const std::vector<alarm_control_panel::AlarmControlPanel *> &get_alarm_control_panels() {
return this->alarm_control_panels_; return this->alarm_control_panels_;
} }
alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
for (auto *obj : this->alarm_control_panels_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
const std::vector<event::Event *> &get_events() { return this->events_; } const std::vector<event::Event *> &get_events() { return this->events_; }
event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(event::Event, event, events)
for (auto *obj : this->events_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
const std::vector<update::UpdateEntity *> &get_updates() { return this->updates_; } const std::vector<update::UpdateEntity *> &get_updates() { return this->updates_; }
update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { GET_ENTITY_METHOD(update::UpdateEntity, update, updates)
for (auto *obj : this->updates_) {
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
}
return nullptr;
}
#endif #endif
Scheduler scheduler; Scheduler scheduler;

View File

@ -26,17 +26,17 @@ static const char *const TAG = "component";
// 1. Components are never destroyed in ESPHome // 1. Components are never destroyed in ESPHome
// 2. Failed components remain failed (no recovery mechanism) // 2. Failed components remain failed (no recovery mechanism)
// 3. Memory usage is minimal (only failures with custom messages are stored) // 3. Memory usage is minimal (only failures with custom messages are stored)
static std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> &get_component_error_messages() {
static std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> instance;
return instance;
}
// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
// This is safe because ESPHome is single-threaded during initialization
namespace {
// Error messages for failed components
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> component_error_messages;
// Setup priority overrides - freed after setup completes // Setup priority overrides - freed after setup completes
// Typically < 5 entries, lazy allocated // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static std::unique_ptr<std::vector<std::pair<const Component *, float>>> &get_setup_priority_overrides() { std::unique_ptr<std::vector<std::pair<const Component *, float>>> setup_priority_overrides;
static std::unique_ptr<std::vector<std::pair<const Component *, float>>> instance; } // namespace
return instance;
}
namespace setup_priority { namespace setup_priority {
@ -130,8 +130,8 @@ void Component::call_dump_config() {
if (this->is_failed()) { if (this->is_failed()) {
// Look up error message from global vector // Look up error message from global vector
const char *error_msg = "unspecified"; const char *error_msg = "unspecified";
if (get_component_error_messages()) { if (component_error_messages) {
for (const auto &pair : *get_component_error_messages()) { for (const auto &pair : *component_error_messages) {
if (pair.first == this) { if (pair.first == this) {
error_msg = pair.second; error_msg = pair.second;
break; break;
@ -285,18 +285,18 @@ void Component::status_set_error(const char *message) {
ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
if (strcmp(message, "unspecified") != 0) { if (strcmp(message, "unspecified") != 0) {
// Lazy allocate the error messages vector if needed // Lazy allocate the error messages vector if needed
if (!get_component_error_messages()) { if (!component_error_messages) {
get_component_error_messages() = std::make_unique<std::vector<std::pair<const Component *, const char *>>>(); component_error_messages = std::make_unique<std::vector<std::pair<const Component *, const char *>>>();
} }
// Check if this component already has an error message // Check if this component already has an error message
for (auto &pair : *get_component_error_messages()) { for (auto &pair : *component_error_messages) {
if (pair.first == this) { if (pair.first == this) {
pair.second = message; pair.second = message;
return; return;
} }
} }
// Add new error message // Add new error message
get_component_error_messages()->emplace_back(this, message); component_error_messages->emplace_back(this, message);
} }
} }
void Component::status_clear_warning() { void Component::status_clear_warning() {
@ -322,9 +322,9 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
void Component::dump_config() {} void Component::dump_config() {}
float Component::get_actual_setup_priority() const { float Component::get_actual_setup_priority() const {
// Check if there's an override in the global vector // Check if there's an override in the global vector
if (get_setup_priority_overrides()) { if (setup_priority_overrides) {
// Linear search is fine for small n (typically < 5 overrides) // Linear search is fine for small n (typically < 5 overrides)
for (const auto &pair : *get_setup_priority_overrides()) { for (const auto &pair : *setup_priority_overrides) {
if (pair.first == this) { if (pair.first == this) {
return pair.second; return pair.second;
} }
@ -334,14 +334,14 @@ float Component::get_actual_setup_priority() const {
} }
void Component::set_setup_priority(float priority) { void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed // Lazy allocate the vector if needed
if (!get_setup_priority_overrides()) { if (!setup_priority_overrides) {
get_setup_priority_overrides() = std::make_unique<std::vector<std::pair<const Component *, float>>>(); setup_priority_overrides = std::make_unique<std::vector<std::pair<const Component *, float>>>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides) // Reserve some space to avoid reallocations (most configs have < 10 overrides)
get_setup_priority_overrides()->reserve(10); setup_priority_overrides->reserve(10);
} }
// Check if this component already has an override // Check if this component already has an override
for (auto &pair : *get_setup_priority_overrides()) { for (auto &pair : *setup_priority_overrides) {
if (pair.first == this) { if (pair.first == this) {
pair.second = priority; pair.second = priority;
return; return;
@ -349,7 +349,7 @@ void Component::set_setup_priority(float priority) {
} }
// Add new override // Add new override
get_setup_priority_overrides()->emplace_back(this, priority); setup_priority_overrides->emplace_back(this, priority);
} }
bool Component::has_overridden_loop() const { bool Component::has_overridden_loop() const {
@ -414,7 +414,7 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
void clear_setup_priority_overrides() { void clear_setup_priority_overrides() {
// Free the setup priority map completely // Free the setup priority map completely
get_setup_priority_overrides().reset(); setup_priority_overrides.reset();
} }
} // namespace esphome } // namespace esphome

View File

@ -6,6 +6,7 @@ from pathlib import Path
from esphome import automation, core from esphome import automation, core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AREA, CONF_AREA,
@ -35,6 +36,7 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VERSION, CONF_VERSION,
KEY_CORE, KEY_CORE,
PlatformFramework,
__version__ as ESPHOME_VERSION, __version__ as ESPHOME_VERSION,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -551,3 +553,16 @@ async def to_code(config: ConfigType) -> None:
cg.add(dev.set_area_id(area_id_hash)) cg.add(dev.set_area_id(area_id_hash))
cg.add(cg.App.register_device(dev)) cg.add(cg.App.register_device(dev))
# Platform-specific source files for core
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"ring_buffer.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
# Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
# as they are only included when needed by the preprocessor
}
)

View File

@ -33,6 +33,7 @@
#define USE_DEEP_SLEEP #define USE_DEEP_SLEEP
#define USE_DEVICES #define USE_DEVICES
#define USE_DISPLAY #define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT #define USE_EVENT
#define USE_FAN #define USE_FAN

View File

@ -27,12 +27,22 @@ void EntityBase::set_name(const char *name) {
// Entity Icon // Entity Icon
std::string EntityBase::get_icon() const { std::string EntityBase::get_icon() const {
#ifdef USE_ENTITY_ICON
if (this->icon_c_str_ == nullptr) { if (this->icon_c_str_ == nullptr) {
return ""; return "";
} }
return this->icon_c_str_; return this->icon_c_str_;
#else
return "";
#endif
}
void EntityBase::set_icon(const char *icon) {
#ifdef USE_ENTITY_ICON
this->icon_c_str_ = icon;
#else
// No-op when USE_ENTITY_ICON is not defined
#endif
} }
void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
// Entity Object ID // Entity Object ID
std::string EntityBase::get_object_id() const { std::string EntityBase::get_object_id() const {

View File

@ -80,7 +80,9 @@ class EntityBase {
StringRef name_; StringRef name_;
const char *object_id_c_str_{nullptr}; const char *object_id_c_str_{nullptr};
#ifdef USE_ENTITY_ICON
const char *icon_c_str_{nullptr}; const char *icon_c_str_{nullptr};
#endif
uint32_t object_id_hash_{}; uint32_t object_id_hash_{};
#ifdef USE_DEVICES #ifdef USE_DEVICES
Device *device_{}; Device *device_{};

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