diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca6d1b0aac..b7546012e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,17 +214,51 @@ jobs: if: matrix.os == 'windows-latest' run: | ./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 if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' run: | . 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 uses: codecov/codecov-action@v5.4.3 with: 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: name: Check clang-format runs-on: ubuntu-24.04 @@ -494,6 +528,7 @@ jobs: - flake8 - pylint - pytest + - integration-tests - pyupgrade - clang-tidy - list-components diff --git a/CODEOWNERS b/CODEOWNERS index 1a7dc4f227..9b4681fcf2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -170,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow esphome/components/ft63x6/* @gpambrozio esphome/components/gcja5/* @gcormier esphome/components/gdk101/* @Szewcson +esphome/components/gl_r01_i2c/* @pkejval esphome/components/globals/* @esphome/core esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp8403/* @jesserockz @@ -254,6 +255,7 @@ esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/logger/select/* @clydebarrow +esphome/components/lps22/* @nagisa esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr501/* @latonita esphome/components/ltr_als_ps/* @latonita @@ -442,6 +444,7 @@ esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 esphome/components/switch/* @esphome/core esphome/components/switch/binary_sensor/* @ssieb +esphome/components/sx126x/* @swoboda1337 esphome/components/sx127x/* @swoboda1337 esphome/components/syslog/* @clydebarrow esphome/components/t6615/* @tylermenezes diff --git a/Doxyfile b/Doxyfile index 03d432b924..1f5ac5aa1b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.0-dev +PROJECT_NUMBER = 2025.8.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 5f94c61a08..10b7df8638 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -10,8 +10,15 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform 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 CODEOWNERS = ["@esphome/core"] @@ -229,3 +236,20 @@ def validate_adc_pin(value): )(value) 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, + }, + } +) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 4a6ce371e5..b736e6b8b0 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -23,7 +23,7 @@ void APDS9960::setup() { 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->mark_failed(); return; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 2f1be28293..eb8883b025 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -3,6 +3,7 @@ import base64 from esphome import automation from esphome.automation import Condition import esphome.codegen as cg +from esphome.config_helpers import get_logger_level import esphome.config_validation as cv from esphome.const import ( 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, {}) async def api_connected_to_code(config, condition_id, template_arg, args): 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 [] diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 51a5769f99..779784e787 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -42,6 +42,19 @@ static const char *const TAG = "api.connection"; static const int CAMERA_STOP_STREAM = 5000; #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 sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { #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); } void APIConnection::cover_command(const CoverCommandRequest &msg) { - cover::Cover *cover = App.get_cover_by_key(msg.key); - if (cover == nullptr) - return; - - auto call = cover->make_call(); + ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) if (msg.has_legacy_command) { switch (msg.legacy_command) { 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); } void APIConnection::fan_command(const FanCommandRequest &msg) { - fan::Fan *fan = App.get_fan_by_key(msg.key); - if (fan == nullptr) - return; - - auto call = fan->make_call(); + ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) if (msg.has_state) call.set_state(msg.state); 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); } void APIConnection::light_command(const LightCommandRequest &msg) { - light::LightState *light = App.get_light_by_key(msg.key); - if (light == nullptr) - return; - - auto call = light->make_call(); + ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) if (msg.has_state) call.set_state(msg.state); 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); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { - switch_::Switch *a_switch = App.get_switch_by_key(msg.key); - if (a_switch == nullptr) - return; + ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) if (msg.state) { 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); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { - climate::Climate *climate = App.get_climate_by_key(msg.key); - if (climate == nullptr) - return; - - auto call = climate->make_call(); + ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) if (msg.has_mode) call.set_mode(static_cast(msg.mode)); 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); } void APIConnection::number_command(const NumberCommandRequest &msg) { - number::Number *number = App.get_number_by_key(msg.key); - if (number == nullptr) - return; - - auto call = number->make_call(); + ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) call.set_value(msg.state); 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); } void APIConnection::date_command(const DateCommandRequest &msg) { - datetime::DateEntity *date = App.get_date_by_key(msg.key); - if (date == nullptr) - return; - - auto call = date->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) call.set_date(msg.year, msg.month, msg.day); 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); } void APIConnection::time_command(const TimeCommandRequest &msg) { - datetime::TimeEntity *time = App.get_time_by_key(msg.key); - if (time == nullptr) - return; - - auto call = time->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) call.set_time(msg.hour, msg.minute, msg.second); 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); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { - datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); - if (datetime == nullptr) - return; - - auto call = datetime->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) call.set_datetime(msg.epoch_seconds); 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); } void APIConnection::text_command(const TextCommandRequest &msg) { - text::Text *text = App.get_text_by_key(msg.key); - if (text == nullptr) - return; - - auto call = text->make_call(); + ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) call.set_value(msg.state); 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); } void APIConnection::select_command(const SelectCommandRequest &msg) { - select::Select *select = App.get_select_by_key(msg.key); - if (select == nullptr) - return; - - auto call = select->make_call(); + ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) call.set_option(msg.state); 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); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { - button::Button *button = App.get_button_by_key(msg.key); - if (button == nullptr) - return; - + ENTITY_COMMAND_GET(button::Button, button, button) button->press(); } #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); } void APIConnection::lock_command(const LockCommandRequest &msg) { - lock::Lock *a_lock = App.get_lock_by_key(msg.key); - if (a_lock == nullptr) - return; + ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) switch (msg.command) { 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); } void APIConnection::valve_command(const ValveCommandRequest &msg) { - valve::Valve *valve = App.get_valve_by_key(msg.key); - if (valve == nullptr) - return; - - auto call = valve->make_call(); + ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) if (msg.has_position) call.set_position(msg.position); 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); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { - media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); - if (media_player == nullptr) - return; - - auto call = media_player->make_call(); + ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) if (msg.has_command) { call.set_command(static_cast(msg.command)); } @@ -1218,66 +1176,53 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ #endif #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) { if (voice_assistant::global_voice_assistant != nullptr) { voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); } } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } + if (!this->check_voice_assistant_api_connection_()) { + return; + } - if (msg.error) { - voice_assistant::global_voice_assistant->failed_to_start(); - return; - } - if (msg.port == 0) { - // Use API Audio - voice_assistant::global_voice_assistant->start_streaming(); - } else { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - this->helper_->getpeername((struct sockaddr *) &storage, &len); - voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); - } + if (msg.error) { + voice_assistant::global_voice_assistant->failed_to_start(); + return; + } + if (msg.port == 0) { + // Use API Audio + voice_assistant::global_voice_assistant->start_streaming(); + } else { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + this->helper_->getpeername((struct sockaddr *) &storage, &len); + voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); } }; void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_event(msg); } } void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_audio(msg); } }; void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_timer_event(msg); } }; void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { 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( const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - 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; + if (!this->check_voice_assistant_api_connection_()) { + 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; return resp; } void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { 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); } 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); - if (a_alarm_control_panel == nullptr) - return; - - auto call = a_alarm_control_panel->make_call(); + ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) switch (msg.command) { case enums::ALARM_CONTROL_PANEL_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); } void APIConnection::update_command(const UpdateCommandRequest &msg) { - update::UpdateEntity *update = App.get_update_by_key(msg.key); - if (update == nullptr) - return; + ENTITY_COMMAND_GET(update::UpdateEntity, update, update) switch (msg.command) { case enums::UPDATE_COMMAND_UPDATE: @@ -1459,12 +1392,11 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { } #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) return false; // Pre-calculate message size to avoid reallocations - const size_t line_length = strlen(line); uint32_t msg_size = 0; // 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) // 1 byte for field tag + size of length varint + string length - msg_size += 1 + api::ProtoSize::varint(static_cast(line_length)) + line_length; + msg_size += 1 + api::ProtoSize::varint(static_cast(message_len)) + message_len; // Create a pre-sized buffer auto buffer = this->create_buffer(msg_size); // Encode the message (SubscribeLogsResponse) buffer.encode_uint32(1, static_cast(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 return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 166dbc3656..b70b037999 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection { bool send_media_player_state(media_player::MediaPlayer *media_player); void media_player_command(const MediaPlayerCommandRequest &msg) override; #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) { if (!this->flags_.service_call_subscription) return; @@ -301,6 +301,11 @@ class APIConnection : public APIServerConnection { static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, 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 template void process_iterator_batch_(Iterator &iterator) { size_t initial_size = this->deferred_batch_.size(); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 6ed9c95354..2f5acc3bfa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -225,6 +225,22 @@ APIError APIFrameHelper::init_common_() { } #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 //#define HELPER_LOG_PACKETS @@ -327,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // no header information yet uint8_t to_read = 3 - rx_header_buf_len_; ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - 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; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_header_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { @@ -372,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read uint16_t to_read = msg_size - rx_buf_len_; ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - 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; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_buf_len_ += static_cast(received); if (static_cast(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 ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); - 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; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } // 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 uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - 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; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 1bb6bc7ed3..eae83a3484 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -176,6 +176,9 @@ class APIFrameHelper { // Common initialization for both plaintext and noise protocols APIError init_common_(); + + // Helper method to handle socket read results + APIError handle_socket_read_result_(ssize_t received); }; #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 84e765e40f..48ddd42d61 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -600,12 +600,12 @@ void HelloRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" api_version_major: "); - sprintf(buffer, "%" PRIu32, this->api_version_major); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); out.append(buffer); out.append("\n"); out.append(" api_version_minor: "); - sprintf(buffer, "%" PRIu32, this->api_version_minor); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); out.append(buffer); out.append("\n"); out.append("}"); @@ -614,12 +614,12 @@ void HelloResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("HelloResponse {\n"); out.append(" api_version_major: "); - sprintf(buffer, "%" PRIu32, this->api_version_major); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); out.append(buffer); out.append("\n"); out.append(" api_version_minor: "); - sprintf(buffer, "%" PRIu32, this->api_version_minor); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); out.append(buffer); out.append("\n"); @@ -657,7 +657,7 @@ void AreaInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AreaInfo {\n"); out.append(" area_id: "); - sprintf(buffer, "%" PRIu32, this->area_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); out.append(buffer); out.append("\n"); @@ -670,7 +670,7 @@ void DeviceInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfo {\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); @@ -679,7 +679,7 @@ void DeviceInfo::dump_to(std::string &out) const { out.append("\n"); out.append(" area_id: "); - sprintf(buffer, "%" PRIu32, this->area_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -724,17 +724,17 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" webserver_port: "); - sprintf(buffer, "%" PRIu32, this->webserver_port); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->webserver_port); out.append(buffer); out.append("\n"); out.append(" legacy_bluetooth_proxy_version: "); - sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version); out.append(buffer); out.append("\n"); out.append(" bluetooth_proxy_feature_flags: "); - sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags); out.append(buffer); out.append("\n"); @@ -747,12 +747,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_voice_assistant_version: "); - sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); out.append(buffer); out.append("\n"); out.append(" voice_assistant_feature_flags: "); - sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); out.append(buffer); out.append("\n"); @@ -797,7 +797,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -830,7 +830,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -839,7 +839,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BinarySensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -852,7 +852,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -867,7 +867,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -912,7 +912,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -921,7 +921,7 @@ void CoverStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CoverStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -930,12 +930,12 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); out.append(" tilt: "); - sprintf(buffer, "%g", this->tilt); + snprintf(buffer, sizeof(buffer), "%g", this->tilt); out.append(buffer); out.append("\n"); @@ -944,7 +944,7 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -953,7 +953,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CoverCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -970,7 +970,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); @@ -979,7 +979,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" tilt: "); - sprintf(buffer, "%g", this->tilt); + snprintf(buffer, sizeof(buffer), "%g", this->tilt); out.append(buffer); out.append("\n"); @@ -998,7 +998,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1023,7 +1023,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" supported_speed_count: "); - sprintf(buffer, "%" PRId32, this->supported_speed_count); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->supported_speed_count); out.append(buffer); out.append("\n"); @@ -1046,7 +1046,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { } out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1055,7 +1055,7 @@ void FanStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("FanStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1076,7 +1076,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" speed_level: "); - sprintf(buffer, "%" PRId32, this->speed_level); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); @@ -1085,7 +1085,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1094,7 +1094,7 @@ void FanCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("FanCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1135,7 +1135,7 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" speed_level: "); - sprintf(buffer, "%" PRId32, this->speed_level); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); @@ -1158,7 +1158,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1193,12 +1193,12 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" min_mireds: "); - sprintf(buffer, "%g", this->min_mireds); + snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); out.append(buffer); out.append("\n"); out.append(" max_mireds: "); - sprintf(buffer, "%g", this->max_mireds); + snprintf(buffer, sizeof(buffer), "%g", this->max_mireds); out.append(buffer); out.append("\n"); @@ -1221,7 +1221,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1230,7 +1230,7 @@ void LightStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LightStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1239,7 +1239,7 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" brightness: "); - sprintf(buffer, "%g", this->brightness); + snprintf(buffer, sizeof(buffer), "%g", this->brightness); out.append(buffer); out.append("\n"); @@ -1248,42 +1248,42 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" color_brightness: "); - sprintf(buffer, "%g", this->color_brightness); + snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); out.append(buffer); out.append("\n"); out.append(" red: "); - sprintf(buffer, "%g", this->red); + snprintf(buffer, sizeof(buffer), "%g", this->red); out.append(buffer); out.append("\n"); out.append(" green: "); - sprintf(buffer, "%g", this->green); + snprintf(buffer, sizeof(buffer), "%g", this->green); out.append(buffer); out.append("\n"); out.append(" blue: "); - sprintf(buffer, "%g", this->blue); + snprintf(buffer, sizeof(buffer), "%g", this->blue); out.append(buffer); out.append("\n"); out.append(" white: "); - sprintf(buffer, "%g", this->white); + snprintf(buffer, sizeof(buffer), "%g", this->white); out.append(buffer); out.append("\n"); out.append(" color_temperature: "); - sprintf(buffer, "%g", this->color_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); out.append(buffer); out.append("\n"); out.append(" cold_white: "); - sprintf(buffer, "%g", this->cold_white); + snprintf(buffer, sizeof(buffer), "%g", this->cold_white); out.append(buffer); out.append("\n"); out.append(" warm_white: "); - sprintf(buffer, "%g", this->warm_white); + snprintf(buffer, sizeof(buffer), "%g", this->warm_white); out.append(buffer); out.append("\n"); @@ -1292,7 +1292,7 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1301,7 +1301,7 @@ void LightCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LightCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1318,7 +1318,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" brightness: "); - sprintf(buffer, "%g", this->brightness); + snprintf(buffer, sizeof(buffer), "%g", this->brightness); out.append(buffer); out.append("\n"); @@ -1335,7 +1335,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" color_brightness: "); - sprintf(buffer, "%g", this->color_brightness); + snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); out.append(buffer); out.append("\n"); @@ -1344,17 +1344,17 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" red: "); - sprintf(buffer, "%g", this->red); + snprintf(buffer, sizeof(buffer), "%g", this->red); out.append(buffer); out.append("\n"); out.append(" green: "); - sprintf(buffer, "%g", this->green); + snprintf(buffer, sizeof(buffer), "%g", this->green); out.append(buffer); out.append("\n"); out.append(" blue: "); - sprintf(buffer, "%g", this->blue); + snprintf(buffer, sizeof(buffer), "%g", this->blue); out.append(buffer); out.append("\n"); @@ -1363,7 +1363,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" white: "); - sprintf(buffer, "%g", this->white); + snprintf(buffer, sizeof(buffer), "%g", this->white); out.append(buffer); out.append("\n"); @@ -1372,7 +1372,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" color_temperature: "); - sprintf(buffer, "%g", this->color_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); out.append(buffer); out.append("\n"); @@ -1381,7 +1381,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" cold_white: "); - sprintf(buffer, "%g", this->cold_white); + snprintf(buffer, sizeof(buffer), "%g", this->cold_white); out.append(buffer); out.append("\n"); @@ -1390,7 +1390,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" warm_white: "); - sprintf(buffer, "%g", this->warm_white); + snprintf(buffer, sizeof(buffer), "%g", this->warm_white); out.append(buffer); out.append("\n"); @@ -1399,7 +1399,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" transition_length: "); - sprintf(buffer, "%" PRIu32, this->transition_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->transition_length); out.append(buffer); out.append("\n"); @@ -1408,7 +1408,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flash_length: "); - sprintf(buffer, "%" PRIu32, this->flash_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flash_length); out.append(buffer); out.append("\n"); @@ -1431,7 +1431,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1452,7 +1452,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" accuracy_decimals: "); - sprintf(buffer, "%" PRId32, this->accuracy_decimals); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->accuracy_decimals); out.append(buffer); out.append("\n"); @@ -1481,7 +1481,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1490,12 +1490,12 @@ void SensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" state: "); - sprintf(buffer, "%g", this->state); + snprintf(buffer, sizeof(buffer), "%g", this->state); out.append(buffer); out.append("\n"); @@ -1504,7 +1504,7 @@ void SensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1519,7 +1519,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1552,7 +1552,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1561,7 +1561,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SwitchStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1570,7 +1570,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1579,7 +1579,7 @@ void SwitchCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SwitchCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1598,7 +1598,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1627,7 +1627,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1636,7 +1636,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextSensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1649,7 +1649,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1786,7 +1786,7 @@ void GetTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("GetTimeResponse {\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append("}"); @@ -1811,7 +1811,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1830,12 +1830,12 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_int: "); - sprintf(buffer, "%" PRId32, this->legacy_int); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->legacy_int); out.append(buffer); out.append("\n"); out.append(" float_: "); - sprintf(buffer, "%g", this->float_); + snprintf(buffer, sizeof(buffer), "%g", this->float_); out.append(buffer); out.append("\n"); @@ -1844,7 +1844,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" int_: "); - sprintf(buffer, "%" PRId32, this->int_); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->int_); out.append(buffer); out.append("\n"); @@ -1856,14 +1856,14 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { for (const auto &it : this->int_array) { out.append(" int_array: "); - sprintf(buffer, "%" PRId32, it); + snprintf(buffer, sizeof(buffer), "%" PRId32, it); out.append(buffer); out.append("\n"); } for (const auto &it : this->float_array) { out.append(" float_array: "); - sprintf(buffer, "%g", it); + snprintf(buffer, sizeof(buffer), "%g", it); out.append(buffer); out.append("\n"); } @@ -1879,7 +1879,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ExecuteServiceRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1899,7 +1899,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1924,7 +1924,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1933,7 +1933,7 @@ void CameraImageResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CameraImageResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1968,7 +1968,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1995,17 +1995,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { } out.append(" visual_min_temperature: "); - sprintf(buffer, "%g", this->visual_min_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->visual_min_temperature); out.append(buffer); out.append("\n"); out.append(" visual_max_temperature: "); - sprintf(buffer, "%g", this->visual_max_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->visual_max_temperature); out.append(buffer); out.append("\n"); out.append(" visual_target_temperature_step: "); - sprintf(buffer, "%g", this->visual_target_temperature_step); + snprintf(buffer, sizeof(buffer), "%g", this->visual_target_temperature_step); out.append(buffer); out.append("\n"); @@ -2060,7 +2060,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" visual_current_temperature_step: "); - sprintf(buffer, "%g", this->visual_current_temperature_step); + snprintf(buffer, sizeof(buffer), "%g", this->visual_current_temperature_step); out.append(buffer); out.append("\n"); @@ -2073,17 +2073,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" visual_min_humidity: "); - sprintf(buffer, "%g", this->visual_min_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->visual_min_humidity); out.append(buffer); out.append("\n"); out.append(" visual_max_humidity: "); - sprintf(buffer, "%g", this->visual_max_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->visual_max_humidity); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2092,7 +2092,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ClimateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2101,22 +2101,22 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" current_temperature: "); - sprintf(buffer, "%g", this->current_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->current_temperature); out.append(buffer); out.append("\n"); out.append(" target_temperature: "); - sprintf(buffer, "%g", this->target_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); out.append(buffer); out.append("\n"); out.append(" target_temperature_low: "); - sprintf(buffer, "%g", this->target_temperature_low); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); out.append(buffer); out.append("\n"); out.append(" target_temperature_high: "); - sprintf(buffer, "%g", this->target_temperature_high); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); out.append(buffer); out.append("\n"); @@ -2149,17 +2149,17 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" current_humidity: "); - sprintf(buffer, "%g", this->current_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->current_humidity); out.append(buffer); out.append("\n"); out.append(" target_humidity: "); - sprintf(buffer, "%g", this->target_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2168,7 +2168,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ClimateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2185,7 +2185,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_temperature: "); - sprintf(buffer, "%g", this->target_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); out.append(buffer); out.append("\n"); @@ -2194,7 +2194,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_temperature_low: "); - sprintf(buffer, "%g", this->target_temperature_low); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); out.append(buffer); out.append("\n"); @@ -2203,7 +2203,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_temperature_high: "); - sprintf(buffer, "%g", this->target_temperature_high); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); out.append(buffer); out.append("\n"); @@ -2260,7 +2260,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_humidity: "); - sprintf(buffer, "%g", this->target_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); out.append(buffer); out.append("\n"); out.append("}"); @@ -2275,7 +2275,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2292,17 +2292,17 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" min_value: "); - sprintf(buffer, "%g", this->min_value); + snprintf(buffer, sizeof(buffer), "%g", this->min_value); out.append(buffer); out.append("\n"); out.append(" max_value: "); - sprintf(buffer, "%g", this->max_value); + snprintf(buffer, sizeof(buffer), "%g", this->max_value); out.append(buffer); out.append("\n"); out.append(" step: "); - sprintf(buffer, "%g", this->step); + snprintf(buffer, sizeof(buffer), "%g", this->step); out.append(buffer); out.append("\n"); @@ -2327,7 +2327,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2336,12 +2336,12 @@ void NumberStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NumberStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" state: "); - sprintf(buffer, "%g", this->state); + snprintf(buffer, sizeof(buffer), "%g", this->state); out.append(buffer); out.append("\n"); @@ -2350,7 +2350,7 @@ void NumberStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2359,12 +2359,12 @@ void NumberCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NumberCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" state: "); - sprintf(buffer, "%g", this->state); + snprintf(buffer, sizeof(buffer), "%g", this->state); out.append(buffer); out.append("\n"); out.append("}"); @@ -2379,7 +2379,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2410,7 +2410,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2419,7 +2419,7 @@ void SelectStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SelectStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2432,7 +2432,7 @@ void SelectStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2441,7 +2441,7 @@ void SelectCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SelectCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2460,7 +2460,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2499,7 +2499,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2508,7 +2508,7 @@ void SirenStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SirenStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2517,7 +2517,7 @@ void SirenStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2526,7 +2526,7 @@ void SirenCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SirenCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2551,7 +2551,7 @@ void SirenCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" duration: "); - sprintf(buffer, "%" PRIu32, this->duration); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->duration); out.append(buffer); out.append("\n"); @@ -2560,7 +2560,7 @@ void SirenCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" volume: "); - sprintf(buffer, "%g", this->volume); + snprintf(buffer, sizeof(buffer), "%g", this->volume); out.append(buffer); out.append("\n"); out.append("}"); @@ -2575,7 +2575,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2616,7 +2616,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2625,7 +2625,7 @@ void LockStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LockStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2634,7 +2634,7 @@ void LockStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2643,7 +2643,7 @@ void LockCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LockCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2670,7 +2670,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2699,7 +2699,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2708,7 +2708,7 @@ void ButtonCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ButtonCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append("}"); @@ -2723,12 +2723,12 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const { out.append("\n"); out.append(" sample_rate: "); - sprintf(buffer, "%" PRIu32, this->sample_rate); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_rate); out.append(buffer); out.append("\n"); out.append(" num_channels: "); - sprintf(buffer, "%" PRIu32, this->num_channels); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->num_channels); out.append(buffer); out.append("\n"); @@ -2737,7 +2737,7 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const { out.append("\n"); out.append(" sample_bytes: "); - sprintf(buffer, "%" PRIu32, this->sample_bytes); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_bytes); out.append(buffer); out.append("\n"); out.append("}"); @@ -2750,7 +2750,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2785,7 +2785,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { } out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2794,7 +2794,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2803,7 +2803,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" volume: "); - sprintf(buffer, "%g", this->volume); + snprintf(buffer, sizeof(buffer), "%g", this->volume); out.append(buffer); out.append("\n"); @@ -2812,7 +2812,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2821,7 +2821,7 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2838,7 +2838,7 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" volume: "); - sprintf(buffer, "%g", this->volume); + snprintf(buffer, sizeof(buffer), "%g", this->volume); out.append(buffer); out.append("\n"); @@ -2865,7 +2865,7 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const __attribute__((unused)) char buffer[64]; out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); out.append(buffer); out.append("\n"); out.append("}"); @@ -2879,7 +2879,7 @@ void BluetoothServiceData::dump_to(std::string &out) const { for (const auto &it : this->legacy_data) { out.append(" legacy_data: "); - sprintf(buffer, "%" PRIu32, it); + snprintf(buffer, sizeof(buffer), "%" PRIu32, it); out.append(buffer); out.append("\n"); } @@ -2893,7 +2893,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothLEAdvertisementResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -2902,7 +2902,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" rssi: "); - sprintf(buffer, "%" PRId32, this->rssi); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); out.append(buffer); out.append("\n"); @@ -2925,7 +2925,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { } out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); out.append("}"); @@ -2934,17 +2934,17 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothLERawAdvertisement {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" rssi: "); - sprintf(buffer, "%" PRId32, this->rssi); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); out.append(buffer); out.append("\n"); out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); @@ -2967,7 +2967,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -2980,7 +2980,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); out.append("}"); @@ -2989,7 +2989,7 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceConnectionResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -2998,12 +2998,12 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" mtu: "); - sprintf(buffer, "%" PRIu32, this->mtu); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->mtu); out.append(buffer); out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3012,7 +3012,7 @@ void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTGetServicesRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append("}"); @@ -3022,13 +3022,13 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const { out.append("BluetoothGATTDescriptor {\n"); for (const auto &it : this->uuid) { out.append(" uuid: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3038,18 +3038,18 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const { out.append("BluetoothGATTCharacteristic {\n"); for (const auto &it : this->uuid) { out.append(" uuid: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append(" properties: "); - sprintf(buffer, "%" PRIu32, this->properties); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->properties); out.append(buffer); out.append("\n"); @@ -3065,13 +3065,13 @@ void BluetoothGATTService::dump_to(std::string &out) const { out.append("BluetoothGATTService {\n"); for (const auto &it : this->uuid) { out.append(" uuid: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3086,7 +3086,7 @@ void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTGetServicesResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3101,7 +3101,7 @@ void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTGetServicesDoneResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append("}"); @@ -3110,12 +3110,12 @@ void BluetoothGATTReadRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTReadRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3124,12 +3124,12 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTReadResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3142,12 +3142,12 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTWriteRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3164,12 +3164,12 @@ void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTReadDescriptorRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3178,12 +3178,12 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTWriteDescriptorRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3196,12 +3196,12 @@ void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTNotifyRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3214,12 +3214,12 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTNotifyDataResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3235,18 +3235,18 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothConnectionsFreeResponse {\n"); out.append(" free: "); - sprintf(buffer, "%" PRIu32, this->free); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->free); out.append(buffer); out.append("\n"); out.append(" limit: "); - sprintf(buffer, "%" PRIu32, this->limit); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->limit); out.append(buffer); out.append("\n"); for (const auto &it : this->allocated) { out.append(" allocated: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } @@ -3256,17 +3256,17 @@ void BluetoothGATTErrorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTErrorResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3275,12 +3275,12 @@ void BluetoothGATTWriteResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTWriteResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3289,12 +3289,12 @@ void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTNotifyResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3303,7 +3303,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDevicePairingResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3312,7 +3312,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3321,7 +3321,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceUnpairingResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3330,7 +3330,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3342,7 +3342,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceClearCacheResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3351,7 +3351,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3386,7 +3386,7 @@ void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); out.append(buffer); out.append("\n"); out.append("}"); @@ -3395,17 +3395,17 @@ void VoiceAssistantAudioSettings::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantAudioSettings {\n"); out.append(" noise_suppression_level: "); - sprintf(buffer, "%" PRIu32, this->noise_suppression_level); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->noise_suppression_level); out.append(buffer); out.append("\n"); out.append(" auto_gain: "); - sprintf(buffer, "%" PRIu32, this->auto_gain); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->auto_gain); out.append(buffer); out.append("\n"); out.append(" volume_multiplier: "); - sprintf(buffer, "%g", this->volume_multiplier); + snprintf(buffer, sizeof(buffer), "%g", this->volume_multiplier); out.append(buffer); out.append("\n"); out.append("}"); @@ -3422,7 +3422,7 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); out.append(buffer); out.append("\n"); @@ -3439,7 +3439,7 @@ void VoiceAssistantResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantResponse {\n"); out.append(" port: "); - sprintf(buffer, "%" PRIu32, this->port); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->port); out.append(buffer); out.append("\n"); @@ -3502,12 +3502,12 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" total_seconds: "); - sprintf(buffer, "%" PRIu32, this->total_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->total_seconds); out.append(buffer); out.append("\n"); out.append(" seconds_left: "); - sprintf(buffer, "%" PRIu32, this->seconds_left); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->seconds_left); out.append(buffer); out.append("\n"); @@ -3581,7 +3581,7 @@ void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { } out.append(" max_active_wake_words: "); - sprintf(buffer, "%" PRIu32, this->max_active_wake_words); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_active_wake_words); out.append(buffer); out.append("\n"); out.append("}"); @@ -3606,7 +3606,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3631,7 +3631,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" supported_features: "); - sprintf(buffer, "%" PRIu32, this->supported_features); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->supported_features); out.append(buffer); out.append("\n"); @@ -3644,7 +3644,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3653,7 +3653,7 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AlarmControlPanelStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3662,7 +3662,7 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3671,7 +3671,7 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AlarmControlPanelCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3694,7 +3694,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3719,12 +3719,12 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" min_length: "); - sprintf(buffer, "%" PRIu32, this->min_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->min_length); out.append(buffer); out.append("\n"); out.append(" max_length: "); - sprintf(buffer, "%" PRIu32, this->max_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_length); out.append(buffer); out.append("\n"); @@ -3737,7 +3737,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3746,7 +3746,7 @@ void TextStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3759,7 +3759,7 @@ void TextStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3768,7 +3768,7 @@ void TextCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3787,7 +3787,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3812,7 +3812,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3821,7 +3821,7 @@ void DateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3830,22 +3830,22 @@ void DateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" year: "); - sprintf(buffer, "%" PRIu32, this->year); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); out.append(buffer); out.append("\n"); out.append(" month: "); - sprintf(buffer, "%" PRIu32, this->month); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); out.append(buffer); out.append("\n"); out.append(" day: "); - sprintf(buffer, "%" PRIu32, this->day); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3854,22 +3854,22 @@ void DateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" year: "); - sprintf(buffer, "%" PRIu32, this->year); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); out.append(buffer); out.append("\n"); out.append(" month: "); - sprintf(buffer, "%" PRIu32, this->month); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); out.append(buffer); out.append("\n"); out.append(" day: "); - sprintf(buffer, "%" PRIu32, this->day); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); out.append(buffer); out.append("\n"); out.append("}"); @@ -3884,7 +3884,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3909,7 +3909,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3918,7 +3918,7 @@ void TimeStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TimeStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3927,22 +3927,22 @@ void TimeStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" hour: "); - sprintf(buffer, "%" PRIu32, this->hour); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); out.append(buffer); out.append("\n"); out.append(" minute: "); - sprintf(buffer, "%" PRIu32, this->minute); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); out.append(buffer); out.append("\n"); out.append(" second: "); - sprintf(buffer, "%" PRIu32, this->second); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3951,22 +3951,22 @@ void TimeCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TimeCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" hour: "); - sprintf(buffer, "%" PRIu32, this->hour); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); out.append(buffer); out.append("\n"); out.append(" minute: "); - sprintf(buffer, "%" PRIu32, this->minute); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); out.append(buffer); out.append("\n"); out.append(" second: "); - sprintf(buffer, "%" PRIu32, this->second); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); out.append(buffer); out.append("\n"); out.append("}"); @@ -3981,7 +3981,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4016,7 +4016,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { } out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4025,7 +4025,7 @@ void EventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("EventResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4034,7 +4034,7 @@ void EventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4049,7 +4049,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4090,7 +4090,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4099,12 +4099,12 @@ void ValveStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ValveStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); @@ -4113,7 +4113,7 @@ void ValveStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4122,7 +4122,7 @@ void ValveCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ValveCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4131,7 +4131,7 @@ void ValveCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); @@ -4150,7 +4150,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4175,7 +4175,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4184,7 +4184,7 @@ void DateTimeStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateTimeStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4193,12 +4193,12 @@ void DateTimeStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4207,12 +4207,12 @@ void DateTimeCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateTimeCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append("}"); @@ -4227,7 +4227,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4256,7 +4256,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4265,7 +4265,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("UpdateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4282,7 +4282,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" progress: "); - sprintf(buffer, "%g", this->progress); + snprintf(buffer, sizeof(buffer), "%g", this->progress); out.append(buffer); out.append("\n"); @@ -4307,7 +4307,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4316,7 +4316,7 @@ void UpdateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("UpdateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 575229cf04..0915746381 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -104,18 +104,19 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { - if (this->shutting_down_) { - // Don't try to send logs during shutdown - // as it could result in a recursion and - // we would be filling a buffer we are trying to clear - return; - } - for (auto &c : this->clients_) { - if (!c->flags_.remove) - c->try_send_log_message(level, tag, message); - } - }); + logger::global_logger->add_on_log_callback( + [this](int level, const char *tag, const char *message, size_t message_len) { + if (this->shutting_down_) { + // Don't try to send logs during shutdown + // as it could result in a recursion and + // we would be filling a buffer we are trying to clear + return; + } + for (auto &c : this->clients_) { + if (!c->flags_.remove) + c->try_send_log_message(level, tag, message, message_len); + } + }); } #endif @@ -260,180 +261,114 @@ bool APIServer::check_password(const std::string &password) const { 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 -void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_binary_sensor_state(obj); -} +API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif #ifdef USE_COVER -void APIServer::on_cover_update(cover::Cover *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_cover_state(obj); -} +API_DISPATCH_UPDATE(cover::Cover, cover) #endif #ifdef USE_FAN -void APIServer::on_fan_update(fan::Fan *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_fan_state(obj); -} +API_DISPATCH_UPDATE(fan::Fan, fan) #endif #ifdef USE_LIGHT -void APIServer::on_light_update(light::LightState *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_light_state(obj); -} +API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_sensor_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) #endif #ifdef USE_SWITCH -void APIServer::on_switch_update(switch_::Switch *obj, bool state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_switch_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) #endif #ifdef USE_TEXT_SENSOR -void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_text_sensor_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) #endif #ifdef USE_CLIMATE -void APIServer::on_climate_update(climate::Climate *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_climate_state(obj); -} +API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -void APIServer::on_number_update(number::Number *obj, float state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_number_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) #endif #ifdef USE_DATETIME_DATE -void APIServer::on_date_update(datetime::DateEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_date_state(obj); -} +API_DISPATCH_UPDATE(datetime::DateEntity, date) #endif #ifdef USE_DATETIME_TIME -void APIServer::on_time_update(datetime::TimeEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_time_state(obj); -} +API_DISPATCH_UPDATE(datetime::TimeEntity, time) #endif #ifdef USE_DATETIME_DATETIME -void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_datetime_state(obj); -} +API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -void APIServer::on_text_update(text::Text *obj, const std::string &state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_text_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) #endif #ifdef USE_SELECT -void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_select_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) #endif #ifdef USE_LOCK -void APIServer::on_lock_update(lock::Lock *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_lock_state(obj); -} +API_DISPATCH_UPDATE(lock::Lock, lock) #endif #ifdef USE_VALVE -void APIServer::on_valve_update(valve::Valve *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_valve_state(obj); -} +API_DISPATCH_UPDATE(valve::Valve, valve) #endif #ifdef USE_MEDIA_PLAYER -void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_media_player_state(obj); -} +API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #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) { + if (obj->is_internal()) + return; for (auto &c : this->clients_) c->send_event(obj, event_type); } #endif #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) { + if (obj->is_internal()) + return; for (auto &c : this->clients_) c->send_update_state(obj); } #endif #ifdef USE_ALARM_CONTROL_PANEL -void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_alarm_control_panel_state(obj); -} +API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index bf0adf1efd..a5e8ec0860 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -52,11 +52,21 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return true; } -static constexpr size_t FLUSH_BATCH_SIZE = 8; -static std::vector &get_batch_buffer() { - static std::vector batch_buffer; - return batch_buffer; -} +// Batch size for BLE advertisements to maximize WiFi efficiency +// Each advertisement is up to 80 bytes when packaged (including protocol overhead) +// Most advertisements are 20-30 bytes, allowing even more to fit per packet +// 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 batch_buffer; +} // namespace + +static std::vector &get_batch_buffer() { return batch_buffer; } 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_) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 66a5fe5d81..b084622f4c 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -2,6 +2,7 @@ CODEOWNERS = ["@esphome/core"] +CONF_BYTE_ORDER = "byte_order" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 1955b5d22c..500dfac1fe 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BLOCK, @@ -7,6 +8,7 @@ from esphome.const import ( CONF_FREE, CONF_ID, CONF_LOOP_TIME, + PlatformFramework, ) CODEOWNERS = ["@OttoWinter"] @@ -44,3 +46,21 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) 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, + }, + } +) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 63b359bd5b..05ae60239d 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, pins 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.const import ( VARIANT_ESP32, @@ -11,6 +11,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_DEFAULT, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_WAKEUP_PIN, PLATFORM_ESP32, PLATFORM_ESP8266, + PlatformFramework, ) WAKEUP_PINS = { @@ -114,12 +116,20 @@ def validate_pin_number(value): return value -def validate_config(config): - if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: - raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") - if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: - raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") - return config +def _validate_ex1_wakeup_mode(value): + if value == "ALL_LOW": + esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) + if value == "ANY_LOW": + esp32.only_on_variant( + 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") @@ -146,6 +156,7 @@ WAKEUP_PIN_MODES = { esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") 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, "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.only_on_esp32, + esp32.only_on_variant( + unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1" + ), cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( 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), 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) await cg.register_parented(var, config[CONF_ID]) 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}, + } +) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b4c7a4e05b..8408f902ef 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -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.""" if supported is not None and not isinstance(supported, list): supported = [supported] @@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None): variant = get_esp32_variant() if supported is not None and variant not in supported: 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: raise cv.Invalid( - f"This feature is not available on {', '.join(unsupported)}" + f"{msg_prefix} is not available on {', '.join(unsupported)}" ) return obj diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp new file mode 100644 index 0000000000..310e7bd94a --- /dev/null +++ b/esphome/components/esp32/helpers.cpp @@ -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 +#include +#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 diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 81582eb09a..2c5697df82 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,10 +25,15 @@ namespace esphome { namespace esp32_ble { // 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 -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; #else -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; #endif // Maximum size of the BLE event queue - must be power of 2 for lock-free queue diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp new file mode 100644 index 0000000000..993de710c6 --- /dev/null +++ b/esphome/components/esp8266/helpers.cpp @@ -0,0 +1,31 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_ESP8266 + +#include +#include +// for xt_rsil()/xt_wsr_ps() +#include + +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 diff --git a/esphome/components/gl_r01_i2c/__init__.py b/esphome/components/gl_r01_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp new file mode 100644 index 0000000000..5a24c63525 --- /dev/null +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp @@ -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 diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.h b/esphome/components/gl_r01_i2c/gl_r01_i2c.h new file mode 100644 index 0000000000..9a7aa023fd --- /dev/null +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.h @@ -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 diff --git a/esphome/components/gl_r01_i2c/sensor.py b/esphome/components/gl_r01_i2c/sensor.py new file mode 100644 index 0000000000..9f6f75faf7 --- /dev/null +++ b/esphome/components/gl_r01_i2c/sensor.py @@ -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) diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp new file mode 100644 index 0000000000..fdad4f5cb6 --- /dev/null +++ b/esphome/components/host/helpers.cpp @@ -0,0 +1,57 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_HOST + +#ifndef _WIN32 +#include +#include +#include +#endif +#include +#include +#include + +#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 dist(0, std::numeric_limits::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(handle_); } +void Mutex::lock() { static_cast(handle_)->lock(); } +bool Mutex::try_lock() { return static_cast(handle_)->try_lock(); } +void Mutex::unlock() { static_cast(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 diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 18373edb77..0d32bc97c2 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import esp32 from esphome.components.const import CONF_REQUEST_HEADERS +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ESP8266_DISABLE_SSL_SUPPORT, @@ -13,6 +14,7 @@ from esphome.const import ( CONF_URL, CONF_WATCHDOG_TIMEOUT, PLATFORM_HOST, + PlatformFramework, __version__, ) 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) 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}, + } +) diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index f81703c087..6c9f1e2877 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MOISTURE): sensor.sensor_schema( unit_of_measurement=UNIT_INTENSITY, accuracy_decimals=0, - device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:weather-rainy", ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 6adb9b71aa..4172b23845 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -3,6 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import esp32 +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -18,6 +19,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority 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)}, 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}, + } +) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 5d593ac3d4..f6d8673a08 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER import esphome.config_validation as cv from esphome.const import ( + CONF_DEFAULTS, CONF_DITHER, CONF_FILE, CONF_ICON, @@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque" CONF_CHROMA_KEY = "chroma_key" CONF_ALPHA_CHANNEL = "alpha_channel" CONF_INVERT_ALPHA = "invert_alpha" +CONF_IMAGES = "images" TRANSPARENCY_TYPES = ( CONF_OPAQUE, @@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder): dither, 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): return image.convert("RGBA") @@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder): g = 1 b = 0 rgb = (r << 11) | (g << 5) | b - self.data[self.index] = rgb >> 8 - self.index += 1 - self.data[self.index] = rgb & 0xFF - self.index += 1 + if self.big_endian: + self.data[self.index] = rgb >> 8 + 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.invert_alpha: a ^= 0xFF @@ -364,7 +377,7 @@ def validate_file_shorthand(value): value = cv.string_strict(value) parts = value.strip().split(":") 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: raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.") return download_gh_svg(parts[1], parts[0]) @@ -434,20 +447,29 @@ def validate_type(image_types): 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() - allow_config = IMAGE_TYPE[type].allow_config - if transparency not in allow_config: + if transparency not in type_class.allow_config: 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) if ( invert_alpha 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") + 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): file = Path(file) if is_svg_file(file): @@ -456,31 +478,82 @@ def validate_settings(value): try: Image.open(file) 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 +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( { - cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_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), + **IMAGE_ID_SCHEMA, + **OPTIONS_SCHEMA, } ).add_extra(validate_settings) IMAGE_SCHEMA = BASE_SCHEMA.extend( { 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): """ 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, # or a dictionary of image types each with a list of images -CONFIG_SCHEMA = cv.Any( - cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}), - cv.ensure_list(IMAGE_SCHEMA), -) +# or a dictionary with keys `defaults:` and `images:` + + +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): @@ -585,6 +681,9 @@ async def write_image(config, all_frames=False): total_rows = height * frame_count 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): image.seek(frame_index) pixels = encoder.convert(image.resize((width, height)), path).getdata() diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a7d31c0131..063fc8b0aa 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display, i2c +from esphome.components.esp32 import CONF_CPU_FREQUENCY import esphome.config_validation as cv from esphome.const import ( CONF_FULL_UPDATE_EVERY, @@ -13,7 +14,9 @@ from esphome.const import ( CONF_PAGES, CONF_TRANSFORM, CONF_WAKEUP_PIN, + PLATFORM_ESP32, ) +import esphome.final_validate as fv DEPENDENCIES = ["i2c", "esp32"] 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): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ld2450/button/__init__.py b/esphome/components/ld2450/button/__init__.py index 39671d3a3b..429aa59389 100644 --- a/esphome/components/ld2450/button/__init__.py +++ b/esphome/components/ld2450/button/__init__.py @@ -13,13 +13,13 @@ from esphome.const import ( 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) CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_FACTORY_RESET): button.button_schema( - ResetButton, + FactoryResetButton, device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, @@ -38,7 +38,7 @@ async def to_code(config): if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) 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): b = await button.new_button(restart_config) await cg.register_parented(b, config[CONF_LD2450_ID]) diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp new file mode 100644 index 0000000000..bcac7ada2f --- /dev/null +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -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 diff --git a/esphome/components/ld2450/button/reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h similarity index 65% rename from esphome/components/ld2450/button/reset_button.h rename to esphome/components/ld2450/button/factory_reset_button.h index 73804fa6d6..8e80347119 100644 --- a/esphome/components/ld2450/button/reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -6,9 +6,9 @@ namespace esphome { namespace ld2450 { -class ResetButton : public button::Button, public Parented { +class FactoryResetButton : public button::Button, public Parented { public: - ResetButton() = default; + FactoryResetButton() = default; protected: void press_action() override; diff --git a/esphome/components/ld2450/button/reset_button.cpp b/esphome/components/ld2450/button/reset_button.cpp deleted file mode 100644 index e96ec99cc5..0000000000 --- a/esphome/components/ld2450/button/reset_button.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "reset_button.h" - -namespace esphome { -namespace ld2450 { - -void ResetButton::press_action() { this->parent_->factory_reset(); } - -} // namespace ld2450 -} // namespace esphome diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 4b87f1cea4..8f3b3a3f21 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -18,11 +18,10 @@ namespace esphome { namespace 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 VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; -enum BaudRateStructure : uint8_t { +enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, BAUD_RATE_19200 = 2, BAUD_RATE_38400 = 3, @@ -33,14 +32,13 @@ enum BaudRateStructure : uint8_t { BAUD_RATE_460800 = 8 }; -// Zone type struct -enum ZoneTypeStructure : uint8_t { +enum ZoneType : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2, }; -enum PeriodicDataStructure : uint8_t { +enum PeriodicData : uint8_t { TARGET_X = 4, TARGET_Y = 6, TARGET_SPEED = 8, @@ -48,12 +46,12 @@ enum PeriodicDataStructure : uint8_t { }; enum PeriodicDataValue : uint8_t { - HEAD = 0xAA, - END = 0x55, + HEADER = 0xAA, + FOOTER = 0x55, CHECK = 0x00, }; -enum AckDataStructure : uint8_t { +enum AckData : uint8_t { COMMAND = 6, COMMAND_STATUS = 7, }; @@ -61,11 +59,11 @@ enum AckDataStructure : uint8_t { // Memory-efficient lookup tables struct StringToUint8 { const char *str; - uint8_t value; + const uint8_t value; }; struct Uint8ToString { - uint8_t value; + const uint8_t value; const char *str; }; @@ -75,6 +73,13 @@ constexpr StringToUint8 BAUD_RATES_BY_STR[] = { {"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[] = { {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, @@ -104,28 +109,35 @@ template const char *find_str(const Uint8ToString (&arr)[N], uint8_t v 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 -static const uint8_t CMD_ENABLE_CONF = 0xFF; -static const uint8_t CMD_DISABLE_CONF = 0xFE; -static const uint8_t CMD_VERSION = 0xA0; -static const uint8_t CMD_MAC = 0xA5; -static const uint8_t CMD_RESET = 0xA2; -static const uint8_t CMD_RESTART = 0xA3; -static const uint8_t CMD_BLUETOOTH = 0xA4; -static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; -static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; -static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; -static const uint8_t CMD_SET_BAUD_RATE = 0xA1; -static const uint8_t CMD_QUERY_ZONE = 0xC1; -static const uint8_t CMD_SET_ZONE = 0xC2; +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80; +static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90; +static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_QUERY_ZONE = 0xC1; +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 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; bytes[i * 2] = val & 0xFF; // Store low byte first (little-endian) 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; } -static inline std::string get_direction(int16_t speed) { - static const char *const APPROACHING = "Approaching"; - static const char *const MOVING_AWAY = "Moving away"; - static const char *const STATIONARY = "Stationary"; - - if (speed > 0) { - return MOVING_AWAY; +static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { + if (header_footer[i] != buffer[i]) { + return false; // Mismatch in header/footer + } } - if (speed < 0) { - return APPROACHING; - } - return STATIONARY; + return true; // Valid header/footer } void LD2450Component::setup() { @@ -192,84 +199,93 @@ void LD2450Component::setup() { } 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 - LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); -#endif -#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_); + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); #endif #ifdef USE_SENSOR - LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_); - LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_); - LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_); + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR(" ", "MovingTargetCount", 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_) { - LOG_SENSOR(" ", "NthTargetXSensor", s); + LOG_SENSOR(" ", "TargetX", s); } for (sensor::Sensor *s : this->move_y_sensors_) { - LOG_SENSOR(" ", "NthTargetYSensor", s); - } - for (sensor::Sensor *s : this->move_speed_sensors_) { - LOG_SENSOR(" ", "NthTargetSpeedSensor", s); + LOG_SENSOR(" ", "TargetY", s); } for (sensor::Sensor *s : this->move_angle_sensors_) { - LOG_SENSOR(" ", "NthTargetAngleSensor", s); + LOG_SENSOR(" ", "TargetAngle", s); } for (sensor::Sensor *s : this->move_distance_sensors_) { - LOG_SENSOR(" ", "NthTargetDistanceSensor", s); + LOG_SENSOR(" ", "TargetDistance", s); } 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_) { - LOG_SENSOR(" ", "NthZoneTargetCountSensor", s); - } - for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { - LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s); + LOG_SENSOR(" ", "ZoneTargetCount", s); } 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 #ifdef USE_TEXT_SENSOR - LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); - LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); + ESP_LOGCONFIG(TAG, "Text Sensors:"); + 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_) { - LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s); + LOG_TEXT_SENSOR(" ", "Direction", s); } #endif #ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "PresenceTimeout", this->presence_timeout_number_); for (auto n : this->zone_numbers_) { - LOG_NUMBER(" ", "ZoneX1Number", n.x1); - LOG_NUMBER(" ", "ZoneY1Number", n.y1); - LOG_NUMBER(" ", "ZoneX2Number", n.x2); - LOG_NUMBER(" ", "ZoneY2Number", n.y2); + LOG_NUMBER(" ", "ZoneX1", n.x1); + LOG_NUMBER(" ", "ZoneY1", n.y1); + LOG_NUMBER(" ", "ZoneX2", n.x2); + LOG_NUMBER(" ", "ZoneY2", n.y2); } #endif #ifdef USE_SELECT - LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); - LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_); + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "ZoneType", this->zone_type_select_); #endif -#ifdef USE_NUMBER - LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_); +#ifdef USE_SWITCH + 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 - 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() { 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; 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}; - 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].y1 = zone_parameters[i * 4 + 1]; 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 zone_type_bytes[2] = {static_cast(this->zone_type_), 0x00}; 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, this->zone_config_[i].y2}; ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); } - std::memcpy(cmd_value, zone_type_bytes, 2); - std::memcpy(cmd_value + 2, area_config, 24); + std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes)); + std::memcpy(cmd_value + 2, area_config, sizeof(area_config)); 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); } @@ -342,14 +358,14 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) { } // Extract, store and publish zone details LD2450 buffer -void LD2450Component::process_zone_(uint8_t *buffer) { +void LD2450Component::process_zone_() { uint8_t index, start; for (index = 0; index < MAX_ZONES; index++) { start = 12 + index * 8; - this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start); - this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2); - this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4); - this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6); + this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start); + 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(this->buffer_data_, start + 4); + this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6); #ifdef USE_NUMBER // only one null check as all coordinates are required for a single zone if (this->zone_numbers_[index].x1 != nullptr) { @@ -395,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() { // Send command with values to LD2450 void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { - ESP_LOGV(TAG, "Sending command %02X", command); - // frame header - this->write_array(CMD_FRAME_HEADER, 4); + ESP_LOGV(TAG, "Sending COMMAND %02X", command); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER)); // length bytes - int len = 2; + uint8_t len = 2; if (command_value != nullptr) { len += command_value_len; } - this->write_byte(lowbyte(len)); - this->write_byte(highbyte(len)); - // command - this->write_byte(lowbyte(command)); - this->write_byte(highbyte(command)); + uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); + // command value bytes 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]); } } - // footer - this->write_array(CMD_FRAME_END, 4); + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); // FIXME to remove delay(50); // NOLINT } @@ -423,26 +437,23 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu // 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] // 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 if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { - ESP_LOGV(TAG, "Throttling: %d", this->throttle_); return; } - if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) - ESP_LOGE(TAG, "Invalid message length"); + if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) + ESP_LOGE(TAG, "Invalid length"); return; } - if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header - ESP_LOGE(TAG, "Invalid message header"); + if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + 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; } - if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer - ESP_LOGE(TAG, "Invalid message footer"); - return; - } - + // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately this->last_periodic_millis_ = App.get_loop_component_start_time(); 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 start = 0; int16_t val = 0; - uint8_t index = 0; int16_t tx = 0; int16_t ty = 0; int16_t td = 0; int16_t ts = 0; int16_t angle = 0; - std::string direction{}; + uint8_t index = 0; + Direction direction{DIRECTION_UNDEFINED}; bool is_moving = false; #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; sensor::Sensor *sx = this->move_x_sensors_[index]; 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; if (this->cached_target_data_[index].x != 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; sensor::Sensor *sy = this->move_y_sensors_[index]; 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; if (this->cached_target_data_[index].y != 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; sensor::Sensor *sr = this->move_resolution_sensors_[index]; 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) { sr->publish_state(val); this->cached_target_data_[index].resolution = val; @@ -499,7 +510,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #endif // SPEED 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; if (val) { is_moving = true; @@ -532,7 +543,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { } } // ANGLE - angle = calculate_angle(static_cast(ty), static_cast(td)); + angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); if (tx > 0) { angle = angle * -1; } @@ -547,14 +558,19 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #endif #ifdef USE_TEXT_SENSOR // DIRECTION - direction = get_direction(ts); 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]; if (tsd != nullptr) { 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; } } @@ -678,117 +694,139 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #endif } -bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { - ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); - if (len < 10) { - ESP_LOGE(TAG, "Invalid ack length"); +bool LD2450Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGE(TAG, "Invalid length"); return true; } - if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header - ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]); + if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); return true; } - if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Invalid ack status"); + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGE(TAG, "Invalid status"); return true; } - if (buffer[8] || buffer[9]) { - ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]); + if (this->buffer_data_[8] || this->buffer_data_[9]) { + ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); return true; } - switch (buffer[COMMAND]) { - case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Enable conf command"); + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); break; - case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Disable conf command"); + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); 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 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 break; - case lowbyte(CMD_VERSION): - this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); - ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); + + case CMD_QUERY_VERSION: { + 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 if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(this->version_); + this->version_text_sensor_->publish_state(version); } #endif break; - case lowbyte(CMD_MAC): - if (len < 20) { + } + + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { 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 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 #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); + this->bluetooth_switch_->publish_state(this->bluetooth_on_); } #endif break; - case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Bluetooth command"); + } + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Bluetooth"); 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 if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(false); } #endif 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 if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(true); } #endif 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 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 break; - case lowbyte(CMD_QUERY_ZONE): - ESP_LOGV(TAG, "Query zone conf command"); - this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); + + case CMD_QUERY_ZONE: + ESP_LOGV(TAG, "Query zone conf"); + this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16); this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); } #endif - if (buffer[10] == 0x00) { + if (this->buffer_data_[10] == 0x00) { ESP_LOGV(TAG, "Zone: Disabled"); } - if (buffer[10] == 0x01) { + if (this->buffer_data_[10] == 0x01) { ESP_LOGV(TAG, "Zone: Area detection"); } - if (buffer[10] == 0x02) { + if (this->buffer_data_[10] == 0x02) { ESP_LOGV(TAG, "Zone: Area filter"); } - this->process_zone_(buffer); + this->process_zone_(); 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(); break; + default: break; } @@ -796,55 +834,57 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { } // Read LD2450 buffer data -void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) { +void LD2450Component::readline_(int readch) { if (readch < 0) { - return; + return; // No data available } - if (this->buffer_pos_ < len - 1) { - buffer[this->buffer_pos_++] = readch; - buffer[this->buffer_pos_] = 0; + + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; } 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) { - ESP_LOGV(TAG, "Handle periodic radar data"); - this->handle_periodic_data_(buffer, this->buffer_pos_); + if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && + this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) { + 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 - } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 && - buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) { - ESP_LOGV(TAG, "Handle command ack data"); - if (this->handle_ack_data_(buffer, this->buffer_pos_)) { - this->buffer_pos_ = 0; // Reset position index for next frame + } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message } else { - ESP_LOGV(TAG, "Command ack data invalid"); + ESP_LOGV(TAG, "Ack Data incomplete"); } } } // Set Config Mode - Pre-requisite sending commands void LD2450Component::set_config_mode_(bool enable) { - uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; - uint8_t cmd_value[2] = {0x01, 0x00}; - this->send_command_(cmd, enable ? cmd_value : nullptr, 2); + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); } // Set Bluetooth Enable/Disable void LD2450Component::set_bluetooth(bool enable) { this->set_config_mode_(true); - uint8_t enable_cmd_value[2] = {0x01, 0x00}; - uint8_t disable_cmd_value[2] = {0x00, 0x00}; - this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } // Set Baud rate void LD2450Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; - this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); 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); } // 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 void LD2450Component::get_mac_() { 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 diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 5ddccab638..ae72a0d8cb 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,10 +38,18 @@ namespace ld2450 { // Constants 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_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 struct Target { int16_t x; @@ -67,19 +75,22 @@ struct ZoneOfNumbers { #endif 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 - SUB_BINARY_SENSOR(target) SUB_BINARY_SENSOR(moving_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 #ifdef USE_TEXT_SENSOR - SUB_TEXT_SENSOR(version) SUB_TEXT_SENSOR(mac) + SUB_TEXT_SENSOR(version) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(presence_timeout) #endif #ifdef USE_SELECT SUB_SELECT(baud_rate) @@ -90,12 +101,9 @@ class LD2450Component : public Component, public uart::UARTDevice { SUB_SWITCH(multi_target) #endif #ifdef USE_BUTTON - SUB_BUTTON(reset) + SUB_BUTTON(factory_reset) SUB_BUTTON(restart) #endif -#ifdef USE_NUMBER - SUB_NUMBER(presence_timeout) -#endif public: void setup() override; @@ -138,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice { protected: void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void set_config_mode_(bool enable); - void handle_periodic_data_(uint8_t *buffer, uint8_t len); - bool handle_ack_data_(uint8_t *buffer, uint8_t len); - void process_zone_(uint8_t *buffer); - void readline_(int readch, uint8_t *buffer, uint8_t len); + void handle_periodic_data_(); + bool handle_ack_data_(); + void process_zone_(); + void readline_(int readch); void get_version_(); void get_mac_(); void query_target_tracking_mode_(); @@ -159,13 +167,14 @@ class LD2450Component : public Component, public uart::UARTDevice { uint32_t moving_presence_millis_ = 0; uint16_t throttle_ = 0; uint16_t timeout_ = 5; - uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer 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; + bool bluetooth_on_{false}; Target target_info_[MAX_TARGETS]; Zone zone_config_[MAX_ZONES]; - std::string version_{}; - std::string mac_{}; // Change detection - cache previous values to avoid redundant publishes // 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::min(); // -32768, outside practical sensor range uint16_t resolution = std::numeric_limits::max(); // 65535, unlikely resolution value uint16_t distance = std::numeric_limits::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 - std::string direction = ""; // Empty string, will differ from any real direction } cached_target_data_[MAX_TARGETS]; struct CachedZoneData { diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp new file mode 100644 index 0000000000..b6451860d5 --- /dev/null +++ b/esphome/components/libretiny/helpers.cpp @@ -0,0 +1,35 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_LIBRETINY + +#include "esphome/core/hal.h" + +#include // 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 diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 8302239d6a..baa4507d2f 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component { } virtual ESPColorView get_view_internal(int32_t index) const = 0; - bool effect_active_{false}; ESPColorCorrection correction_{}; + LightState *state_parent_{nullptr}; #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; #endif - LightState *state_parent_{nullptr}; + bool effect_active_{false}; }; class AddressableLightTransformer : public LightTransitionTransformer { @@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer { protected: AddressableLight &light_; - Color target_color_{}; float last_transition_progress_{0.0f}; float accumulated_alpha_{0.0f}; + Color target_color_{}; }; } // namespace light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 39ce5700c6..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -69,8 +69,8 @@ class ESPColorCorrection { protected: uint8_t gamma_table_[256]; uint8_t gamma_reverse_table_[256]; - uint8_t local_brightness_{255}; Color max_brightness_; + uint8_t local_brightness_{255}; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 78b0ac9feb..a3ffe22591 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -2,12 +2,28 @@ #include "light_call.h" #include "light_state.h" #include "esphome/core/log.h" +#include "esphome/core/optional.h" namespace esphome { namespace 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(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) { if (color_mode == ColorMode::UNKNOWN) return LOG_STR("Unknown"); @@ -32,41 +48,43 @@ void LightCall::perform() { const char *name = this->parent_->get_name().c_str(); LightColorValues v = this->validate_(); - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed 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()))); } // Only print state when it's being changed 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())); } - if (this->brightness_.has_value()) { + if (this->has_brightness()) { 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); } - 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, v.get_blue() * 100.0f); } - if (this->white_.has_value()) { + if (this->has_white()) { 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()); } - 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, v.get_warm_white() * 100.0f); } @@ -74,58 +92,57 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH - if (this->publish_) { - ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); + if (this->get_publish_()) { + 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_()) { // TRANSITION - if (this->publish_) { - ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); + if (this->get_publish_()) { + ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, " Effect: 'None'"); } 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_()) { // EFFECT - auto effect = this->effect_; const char *effect_s; - if (effect == 0u) { + if (this->effect_ == 0u) { effect_s = "None"; } 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); } - this->parent_->start_effect_(*this->effect_); + this->parent_->start_effect_(this->effect_); // Also set light color values when starting an effect // For example to turn off the light this->parent_->set_immediately_(v, true); } else { // INSTANT CHANGE - this->parent_->set_immediately_(v, this->publish_); + this->parent_->set_immediately_(v, this->get_publish_()); } if (!this->has_transition_()) { this->parent_->target_state_reached_callback_.call(); } - if (this->publish_) { + if (this->get_publish_()) { this->parent_->publish_state(); } - if (this->save_) { + if (this->get_save_()) { this->parent_->save_remote_values_(); } } @@ -135,82 +152,80 @@ LightColorValues LightCall::validate_() { auto traits = this->parent_->get_traits(); // Color mode check - if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { - ESP_LOGW(TAG, "'%s' does not support color mode %s", name, - LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); - this->color_mode_.reset(); + if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); + this->set_flag_(FLAG_HAS_COLOR_MODE, false); } // 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->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. this->transform_parameters_(); // 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); - this->brightness_.reset(); + this->set_flag_(FLAG_HAS_BRIGHTNESS, false); } // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && - !(color_mode & ColorCapability::BRIGHTNESS)) { + if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s': transitions not supported", name); - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } // 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); - this->color_brightness_.reset(); + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); } // RGB exists check - if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || - (this->blue_.has_value() && *this->blue_ > 0.0f)) { + if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || + (this->has_blue() && this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); - this->red_.reset(); - this->green_.reset(); - this->blue_.reset(); + this->set_flag_(FLAG_HAS_RED, false); + this->set_flag_(FLAG_HAS_GREEN, false); + this->set_flag_(FLAG_HAS_BLUE, false); } } // White value exists check - if (this->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)) { 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 - if (this->color_temperature_.has_value() && + if (this->has_color_temperature() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { 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 - if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || - (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { + if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); - this->cold_white_.reset(); - this->warm_white_.reset(); + this->set_flag_(FLAG_HAS_COLD_WHITE, false); + this->set_flag_(FLAG_HAS_WARM_WHITE, false); } } #define VALIDATE_RANGE_(name_, upper_name, min, max) \ - if (name_##_.has_value()) { \ - auto val = *name_##_; \ + if (this->has_##name_()) { \ + auto val = this->name_##_; \ 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, \ (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) @@ -227,110 +242,116 @@ LightColorValues LightCall::validate_() { 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. - 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). - if (this->brightness_.has_value() && *this->brightness_ == 0.0f) { - this->state_ = optional(false); - this->brightness_ = optional(1.0f); + if (this->has_brightness() && this->brightness_ == 0.0f) { + this->state_ = false; + this->set_flag_(FLAG_HAS_STATE, true); + this->brightness_ = 1.0f; } // 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->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) - this->color_brightness_ = optional(1.0f); + if (this->has_red() || this->has_green() || this->has_blue()) { + if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { + this->color_brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); + } } // Create color values for the light with this call applied. auto v = this->parent_->remote_values; - if (this->color_mode_.has_value()) - v.set_color_mode(*this->color_mode_); - if (this->state_.has_value()) - v.set_state(*this->state_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); - if (this->color_brightness_.has_value()) - v.set_color_brightness(*this->color_brightness_); - if (this->red_.has_value()) - v.set_red(*this->red_); - if (this->green_.has_value()) - v.set_green(*this->green_); - if (this->blue_.has_value()) - v.set_blue(*this->blue_); - if (this->white_.has_value()) - v.set_white(*this->white_); - if (this->color_temperature_.has_value()) - v.set_color_temperature(*this->color_temperature_); - if (this->cold_white_.has_value()) - v.set_cold_white(*this->cold_white_); - if (this->warm_white_.has_value()) - v.set_warm_white(*this->warm_white_); + if (this->has_color_mode()) + v.set_color_mode(this->color_mode_); + if (this->has_state()) + v.set_state(this->state_); + if (this->has_brightness()) + v.set_brightness(this->brightness_); + if (this->has_color_brightness()) + v.set_color_brightness(this->color_brightness_); + if (this->has_red()) + v.set_red(this->red_); + if (this->has_green()) + v.set_green(this->green_); + if (this->has_blue()) + v.set_blue(this->blue_); + if (this->has_white()) + v.set_white(this->white_); + if (this->has_color_temperature()) + v.set_color_temperature(this->color_temperature_); + if (this->has_cold_white()) + v.set_cold_white(this->cold_white_); + if (this->has_warm_white()) + v.set_warm_white(this->warm_white_); v.normalize_color(); // 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); - this->flash_length_.reset(); + this->set_flag_(FLAG_HAS_FLASH, false); } // validate transition length/flash length/effect not used at the same time bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; // If effect is already active, remove effect start - if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { - this->effect_.reset(); + if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { + this->set_flag_(FLAG_HAS_EFFECT, false); } // validate effect index - if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); - this->effect_.reset(); + if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); + this->set_flag_(FLAG_HAS_EFFECT, false); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); - this->transition_length_.reset(); - this->flash_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); + this->set_flag_(FLAG_HAS_FLASH, false); } if (this->has_flash_() && this->has_transition_()) { 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) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; + this->set_flag_(FLAG_HAS_TRANSITION, true); } - 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) - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } if (this->has_transition_() && !supports_transition) { 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 // 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 - 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_()) { 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) { // Auto turn off effect this->effect_ = 0; + this->set_flag_(FLAG_HAS_EFFECT, true); } } // Disable saving for flashes if (this->has_flash_()) - this->save_ = false; + this->set_flag_(FLAG_SAVE, false); return v; } @@ -343,24 +364,27 @@ void LightCall::transform_parameters_() { // - RGBWW lights with color_interlock=true, which also sets "brightness" and // "color_temperature" (without color_interlock, CW/WW are set directly) // - 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()) && // - (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // - !(*this->color_mode_ & ColorCapability::WHITE) && // - !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) && // + (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // + !(this->color_mode_ & ColorCapability::WHITE) && // + !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // 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", this->parent_->get_name().c_str()); - if (this->color_temperature_.has_value()) { - const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); + if (this->has_color_temperature()) { + const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); const float ww_fraction = (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); const float cw_fraction = 1.0f - ww_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->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()) { - this->brightness_ = *this->white_; + if (this->has_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. 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; // 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; } std::set LightCall::get_suitable_color_modes_() { - bool has_white = this->white_.has_value() && *this->white_ > 0.0f; - bool has_ct = this->color_temperature_.has_value(); - bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || - (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); - bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || - (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + bool has_white = this->has_white() && this->white_ > 0.0f; + bool has_ct = this->has_color_temperature(); + bool has_cwww = + (this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f); + bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || + (this->has_red() || this->has_green() || this->has_blue()); #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) #define ENTRY(white, ct, cwww, rgb, ...) \ @@ -491,7 +515,7 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) { return *this; } 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) { 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) { if (this->parent_->get_traits().supports_color_mode(color_mode)) - this->color_mode_ = color_mode; + this->set_color_mode(color_mode); return *this; } 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); return *this; } -LightCall &LightCall::set_state(optional state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_state(bool state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_transition_length(optional transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_transition_length(uint32_t transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_flash_length(optional 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 brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_brightness(float brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_color_mode(optional 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 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 red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_red(float red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_green(optional green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_green(float green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_blue(optional blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_blue(float blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_white(optional white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_white(float white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_color_temperature(optional 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 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 warm_white) { - this->warm_white_ = warm_white; - return *this; -} -LightCall &LightCall::set_warm_white(float warm_white) { - this->warm_white_ = warm_white; - return *this; -} +IMPLEMENT_LIGHT_CALL_SETTER(state, bool, FLAG_HAS_STATE) +IMPLEMENT_LIGHT_CALL_SETTER(transition_length, uint32_t, FLAG_HAS_TRANSITION) +IMPLEMENT_LIGHT_CALL_SETTER(flash_length, uint32_t, FLAG_HAS_FLASH) +IMPLEMENT_LIGHT_CALL_SETTER(brightness, float, FLAG_HAS_BRIGHTNESS) +IMPLEMENT_LIGHT_CALL_SETTER(color_mode, ColorMode, FLAG_HAS_COLOR_MODE) +IMPLEMENT_LIGHT_CALL_SETTER(color_brightness, float, FLAG_HAS_COLOR_BRIGHTNESS) +IMPLEMENT_LIGHT_CALL_SETTER(red, float, FLAG_HAS_RED) +IMPLEMENT_LIGHT_CALL_SETTER(green, float, FLAG_HAS_GREEN) +IMPLEMENT_LIGHT_CALL_SETTER(blue, float, FLAG_HAS_BLUE) +IMPLEMENT_LIGHT_CALL_SETTER(white, float, FLAG_HAS_WHITE) +IMPLEMENT_LIGHT_CALL_SETTER(color_temperature, float, FLAG_HAS_COLOR_TEMPERATURE) +IMPLEMENT_LIGHT_CALL_SETTER(cold_white, float, FLAG_HAS_COLD_WHITE) +IMPLEMENT_LIGHT_CALL_SETTER(warm_white, float, FLAG_HAS_WARM_WHITE) LightCall &LightCall::set_effect(optional effect) { if (effect.has_value()) this->set_effect(*effect); @@ -660,18 +593,22 @@ LightCall &LightCall::set_effect(optional effect) { } LightCall &LightCall::set_effect(uint32_t effect_number) { this->effect_ = effect_number; + this->set_flag_(FLAG_HAS_EFFECT, true); return *this; } LightCall &LightCall::set_effect(optional 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; } LightCall &LightCall::set_publish(bool publish) { - this->publish_ = publish; + this->set_flag_(FLAG_PUBLISH, publish); return *this; } LightCall &LightCall::set_save(bool save) { - this->save_ = save; + this->set_flag_(FLAG_SAVE, save); return *this; } LightCall &LightCall::set_rgb(float red, float green, float blue) { diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index bca2ac7b07..7e04e1a767 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/optional.h" #include "light_color_values.h" #include @@ -10,6 +9,11 @@ namespace light { class LightState; /** 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 { public: @@ -131,6 +135,19 @@ class LightCall { /// Set whether this light call should trigger a save state to recover them at startup.. 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. * * 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. void transform_parameters_(); - bool has_transition_() { return this->transition_length_.has_value(); } - bool has_flash_() { return this->flash_length_.has_value(); } - bool has_effect_() { return this->effect_.has_value(); } + // Bitfield flags - each flag indicates whether a corresponding value has been set. + enum FieldFlags : uint16_t { + 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_; - optional state_; - optional transition_length_; - optional flash_length_; - optional color_mode_; - optional brightness_; - optional color_brightness_; - optional red_; - optional green_; - optional blue_; - optional white_; - optional color_temperature_; - optional cold_white_; - optional warm_white_; - optional effect_; - bool publish_{true}; - bool save_{true}; + + // Light state values - use flags_ to check if a value has been set. + // Group 4-byte aligned members first + uint32_t transition_length_; + uint32_t flash_length_; + uint32_t effect_; + float brightness_; + float color_brightness_; + float red_; + float green_; + float blue_; + float white_; + float color_temperature_; + float cold_white_; + float warm_white_; + + // 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 diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index d8eaa6ae24..5653a8d2a5 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -46,8 +46,7 @@ class LightColorValues { public: /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : color_mode_(ColorMode::UNKNOWN), - state_(0.0f), + : state_(0.0f), brightness_(1.0f), color_brightness_(1.0f), red_(1.0f), @@ -56,7 +55,8 @@ class LightColorValues { white_(1.0f), color_temperature_{0.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, 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); } protected: - ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -303,6 +302,7 @@ class LightColorValues { float color_temperature_; ///< Color Temperature in Mired float cold_white_; float warm_white_; + ColorMode color_mode_; }; } // namespace light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index f21fb8a06e..72cb99223e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t { struct LightStateRTCState { 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) - : color_mode(color_mode), - state(state), - brightness(brightness), + : brightness(brightness), color_brightness(color_brightness), red(red), green(green), @@ -41,10 +39,12 @@ struct LightStateRTCState { white(white), color_temp(color_temp), cold_white(cold_white), - warm_white(warm_white) {} + warm_white(warm_white), + effect(0), + color_mode(color_mode), + state(state) {} LightStateRTCState() = default; - ColorMode color_mode{ColorMode::UNKNOWN}; - bool state{false}; + // Group 4-byte aligned members first float brightness{1.0f}; float color_brightness{1.0f}; float red{1.0f}; @@ -55,6 +55,9 @@ struct LightStateRTCState { float cold_white{1.0f}; float warm_white{1.0f}; 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 @@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component { std::unique_ptr transformer_{nullptr}; /// List of effects for this light. std::vector 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 uint32_t active_effect_index_{}; /// Default transition length for all transitions in ms. @@ -224,15 +229,11 @@ class LightState : public EntityBase, public Component { uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; - /// Whether the light value should be written in the next cycle. bool next_write_{true}; // for effects, true if a transformer (transition) is active. 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. * * "Remote values" are light color values that are reported to the frontend and have a lower diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index a557bd39b1..8d49acff97 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer { // 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); } - bool changing_color_mode_{false}; LightColorValues end_values_{}; LightColorValues intermediate_values_{}; + bool changing_color_mode_{false}; }; class LightFlashTransformer : public LightTransformer { @@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer { protected: LightState &state_; - uint32_t transition_length_; std::unique_ptr transformer_{nullptr}; + uint32_t transition_length_; bool begun_lightstate_restore_; }; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 3d4907aa6e..9ac2999696 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -21,6 +21,7 @@ from esphome.components.libretiny.const import ( COMPONENT_LN882X, COMPONENT_RTL87XX, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, @@ -42,6 +43,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, + PlatformFramework, ) 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) 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, + }, + } +) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index a2c2aa0320..db807f7e53 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -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 // Implementation for ESP8266 with flash string support. // 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, va_list args) { // NOLINT 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) { 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; } @@ -185,7 +206,8 @@ void Logger::loop() { 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->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 // 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); @@ -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_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { +void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 38faf73d84..fb68e75a51 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,7 +143,7 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + void add_on_log_callback(std::function &&callback); // add a listener for log level changes void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } @@ -192,7 +192,7 @@ class Logger : public Component { if (this->baud_rate_ > 0) { 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 @@ -246,7 +246,7 @@ class Logger : public Component { // Large objects (internally aligned) std::map log_levels_{}; - CallbackManager log_callback_{}; + CallbackManager log_callback_{}; CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr 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) { - 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); } @@ -385,7 +385,7 @@ class LoggerMessageTrigger : public Trigger public: explicit LoggerMessageTrigger(Logger *parent, uint8_t 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_) { this->trigger(level, tag, message); } diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 41445fa3b4..2fde0f7d49 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -184,7 +184,9 @@ void HOT Logger::write_msg_(const char *msg) { ) { puts(msg); } 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); } } diff --git a/esphome/components/lps22/__init__.py b/esphome/components/lps22/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp new file mode 100644 index 0000000000..526286ba72 --- /dev/null +++ b/esphome/components/lps22/lps22.cpp @@ -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(encode_uint16(t_buf[1], t_buf[0])); + float temp = TEMPERATURE_SCALE * static_cast(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(p_lsb)); + } + return RetryResult::DONE; +} + +} // namespace lps22 +} // namespace esphome diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h new file mode 100644 index 0000000000..549ea524ea --- /dev/null +++ b/esphome/components/lps22/lps22.h @@ -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 diff --git a/esphome/components/lps22/sensor.py b/esphome/components/lps22/sensor.py new file mode 100644 index 0000000000..87a2106308 --- /dev/null +++ b/esphome/components/lps22/sensor.py @@ -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)) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index ed230d43aa..e32d39cede 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_component +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_DISABLED, @@ -8,6 +9,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICE, CONF_SERVICES, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -108,3 +110,21 @@ async def to_code(config): ) 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, + }, + } +) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f0d5a95d43..1a6fcabf42 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -5,6 +5,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger 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 from esphome.const import ( CONF_AVAILABILITY, @@ -54,6 +55,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PlatformFramework, ) 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): paren = await cg.get_variable(config[CONF_ID]) 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, + }, + } +) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 20e0b4a499..ab7fd15a35 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -57,14 +57,15 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER 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) { - if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = message, - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); - } - }); + logger::global_logger->add_on_log_callback( + [this](int level, const char *tag, const char *message, size_t message_len) { + if (level <= this->log_level_ && this->is_connected()) { + this->publish({.topic = this->log_message_.topic, + .payload = std::string(message, message_len), + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); + } + }); } #endif diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index fb75daf4ba..8adc49d68c 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -1,5 +1,7 @@ import esphome.codegen as cg 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 = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) @@ -8,3 +10,17 @@ nextion_ref = Nextion.operator("ref") CONF_NEXTION_ID = "nextion_id" CONF_PUBLISH_STATE = "publish_state" 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}, + } +) diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 98dea4b513..392481e39a 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -11,6 +11,7 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_COMMAND_SPACING = "command_spacing" CONF_COMPONENT_NAME = "component_name" +CONF_DUMP_DEVICE_INFO = "dump_device_info" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" CONF_FONT_ID = "font_id" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index b6d4cc3f23..3628ac2f63 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti return; 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; } else { this->needs_to_send_update_ = false; diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 420f8f69c5..4254ae45fe 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -15,6 +15,7 @@ from . import Nextion, nextion_ns, nextion_ref from .base_component import ( CONF_AUTO_WAKE_ON_TOUCH, CONF_COMMAND_SPACING, + CONF_DUMP_DEVICE_INFO, CONF_EXIT_REPARSE_ON_START, CONF_MAX_COMMANDS_PER_LOOP, CONF_MAX_QUEUE_SIZE, @@ -57,6 +58,7 @@ CONFIG_SCHEMA = ( cv.positive_time_period_milliseconds, 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_MAX_COMMANDS_PER_LOOP): cv.uint16_t, 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_START_UP_PAGE): cv.uint8_t, 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, } ) @@ -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_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): cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP") diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index d95238bbb4..66e2d26061 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -13,14 +13,11 @@ void Nextion::setup() { this->is_setup_ = false; this->connection_state_.ignore_is_setup_ = true; - // Wake up the nextion - this->send_command_("bkcmd=0"); - this->send_command_("sleep=0"); + // Wake up the nextion and ensure clean communication state + this->send_command_("sleep=0"); // Exit sleep mode if sleeping + this->send_command_("bkcmd=0"); // Disable return data during init sequence - this->send_command_("bkcmd=0"); - this->send_command_("sleep=0"); - - // Reboot it + // Reset device for clean state - critical for reliable communication this->send_command_("rest"); this->connection_state_.ignore_is_setup_ = false; @@ -51,24 +48,19 @@ bool Nextion::check_connect_() { if (this->connection_state_.is_connected_) return true; - // Check if the handshake should be skipped for the Nextion connection - if (this->skip_connection_handshake_) { - // Log the connection status without handshake - ESP_LOGW(TAG, "Connected (no handshake)"); - // Set the connection status to true - this->connection_state_.is_connected_ = true; - // Return true indicating the connection is set - return true; - } - +#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake + this->is_connected_ = true; // Set the connection status to true + return true; // Return true indicating the connection is set +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE if (this->comok_sent_ == 0) { this->reset_(false); this->connection_state_.ignore_is_setup_ = true; this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating - if (this->exit_reparse_on_start_) { - this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); - } +#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START + this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); +#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START this->send_command_("connect"); 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++) { 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"); comok_sent_ = 0; @@ -118,11 +110,19 @@ bool Nextion::check_connect_() { this->is_detected_ = (connect_info.size() == 7); if (this->is_detected_) { ESP_LOGN(TAG, "Connect info: %zu", connect_info.size()); - +#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO this->device_model_ = connect_info[2]; this->firmware_version_ = connect_info[3]; this->serial_number_ = connect_info[5]; 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 { 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->dump_config(); return true; +#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE } void Nextion::reset_(bool reset_nextion) { @@ -144,29 +145,33 @@ void Nextion::reset_(bool reset_nextion) { void Nextion::dump_config() { ESP_LOGCONFIG(TAG, "Nextion:"); - if (this->skip_connection_handshake_) { - ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_)); - } else { - ESP_LOGCONFIG(TAG, - " 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()); - } + +#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGCONFIG(TAG, " Skip handshake: YES"); +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE 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" - " Exit reparse: %s", - YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_)); + " Touch Timeout: %" PRIu16, +#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 ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->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) { 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_); } + if (this->touch_sleep_timeout_ != 0) { + this->set_touch_sleep_timeout(this->touch_sleep_timeout_); + } + this->connection_state_.ignore_is_setup_ = false; } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 0ce9429594..e2c4faa1d0 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -932,21 +932,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ 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 * @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); /** - * Set the touch sleep timeout of the display. - * @param timeout Timeout in seconds. + * Set the touch sleep timeout of the display using the `thsp` command. + * + * 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: * ```cpp + * // Set 30 second touch timeout * 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 - * `thup`. + * Related Nextion instruction: `thsp=` + * + * @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. @@ -1236,20 +1242,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ 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. * @@ -1292,7 +1284,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * the Nextion display. A connection is considered established when: * * - 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: * - Troubleshooting communication issues @@ -1358,8 +1350,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_CONF_START_UP_PAGE uint8_t start_up_page_ = 255; #endif // USE_NEXTION_CONF_START_UP_PAGE - bool exit_reparse_on_start_ = false; - bool skip_connection_handshake_ = false; + bool auto_wake_on_touch_ = true; /** * 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 writer_; optional brightness_; +#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO std::string device_model_; std::string firmware_version_; std::string serial_number_; std::string flash_size_; +#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO void remove_front_no_sensors_(); diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 018f8fe732..f3a282717b 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -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); } -void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) { - if (touch_sleep_timeout < 3) { - ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)"); - return; +void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { + // Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables) + if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) { + 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", touch_sleep_timeout, true); + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true); } void Nextion::sleep(bool sleep) { diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index cfb4e3600c..32929d6845 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) { return; // This is a variable. no need to set color } this->bco_ = bco; - this->bco_needs_update_ = true; - this->bco_is_set_ = true; + this->component_flags_.bco_needs_update = true; + this->component_flags_.bco_is_set = true; this->update_component_settings(); } @@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) { } this->bco2_ = bco2; - this->bco2_needs_update_ = true; - this->bco2_is_set_ = true; + this->component_flags_.bco2_needs_update = true; + this->component_flags_.bco2_is_set = true; 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 } this->pco_ = pco; - this->pco_needs_update_ = true; - this->pco_is_set_ = true; + this->component_flags_.pco_needs_update = true; + this->component_flags_.pco_is_set = true; 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 } this->pco2_ = pco2; - this->pco2_needs_update_ = true; - this->pco2_is_set_ = true; + this->component_flags_.pco2_needs_update = true; + this->component_flags_.pco2_is_set = true; 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 } this->font_id_ = font_id; - this->font_id_needs_update_ = true; - this->font_id_is_set_ = true; + this->component_flags_.font_id_needs_update = true; + this->component_flags_.font_id_is_set = true; this->update_component_settings(); } @@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) { if (this->variable_name_ == this->variable_name_to_send_) { return; // This is a variable. no need to set color } - this->visible_ = visible; - this->visible_needs_update_ = true; - this->visible_is_set_ = true; + this->component_flags_.visible = visible; + this->component_flags_.visible_needs_update = true; + this->component_flags_.visible_is_set = true; this->update_component_settings(); } void NextionComponent::update_component_settings(bool force_update) { - if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ || - (!this->visible_needs_update_ && !this->visible_)) { + if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set || + (!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) { this->needs_to_send_update_ = true; 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_; 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); } - 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->send_state_to_nextion(); } 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->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->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->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->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->font_id_needs_update_ = false; + this->component_flags_.font_id_needs_update = false; } } } // namespace nextion diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h index 2f3c4f3c16..add9e11cf1 100644 --- a/esphome/components/nextion/nextion_component.h +++ b/esphome/components/nextion/nextion_component.h @@ -21,29 +21,64 @@ class NextionComponent : public NextionComponentBase { void set_visible(bool visible); 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_; - bool bco_needs_update_ = false; - bool bco_is_set_ = false; - Color bco_; - bool bco2_needs_update_ = false; - bool bco2_is_set_ = false; - 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_; + // Color and styling properties + Color bco_; // Background color + Color bco2_; // Pressed background color + Color pco_; // Foreground color + Color pco2_; // Pressed foreground color 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; - bool visible_is_set_ = false; + /** + * @brief Component state management using compact bitfield structure + * + * 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 esphome diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 0ed9da95d4..03b7261239 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { if (this->wave_chan_id_ == UINT8_MAX) { 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; } else { this->needs_to_send_update_ = false; diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index fe71182496..21636f2bfa 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { return; 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; } else { this->needs_to_send_update_ = false; diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index e08cbb02ca..9b6deeda87 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s return; 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; } else { this->nextion_->add_no_result_to_queue_with_set(this, state); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index cf5a7f5ef1..d3a2481693 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -1,5 +1,6 @@ #include "nfc.h" #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -7,29 +8,9 @@ namespace nfc { static const char *const TAG = "nfc"; -std::string format_uid(std::vector &uid) { - 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_uid(const std::vector &uid) { return format_hex_pretty(uid, '-', false); } -std::string format_bytes(std::vector &bytes) { - 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); -} +std::string format_bytes(const std::vector &bytes) { return format_hex_pretty(bytes, ' ', false); } uint8_t guess_tag_type(uint8_t uid_length) { if (uid_length == 4) { diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 2e5c5cd9c5..9879cfdb03 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -2,8 +2,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "ndef_record.h" #include "ndef_message.h" +#include "ndef_record.h" #include "nfc_tag.h" #include @@ -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 MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; -std::string format_uid(std::vector &uid); -std::string format_bytes(std::vector &bytes); +std::string format_uid(const std::vector &uid); +std::string format_bytes(const std::vector &bytes); uint8_t guess_tag_type(uint8_t uid_length); uint8_t get_mifare_classic_ndef_start_index(std::vector &data); diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 627c55e910..4d5b8a61e2 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,5 +1,6 @@ from esphome import automation import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -7,6 +8,7 @@ from esphome.const import ( CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -120,3 +122,18 @@ async def ota_to_code(var, config): use_state_callback = True if use_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, + }, + } +) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 5de7d8c9c4..dffc088085 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg 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 from esphome.const import ( CONF_BUFFER_SIZE, @@ -15,6 +16,7 @@ from esphome.const import ( CONF_TYPE, CONF_USE_DMA, CONF_VALUE, + PlatformFramework, ) 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_filter_us(config[CONF_FILTER])) 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, + }, + } +) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 713cee0186..47a46ff56b 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg 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 from esphome.const import ( CONF_CARRIER_DUTY_PERCENT, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_PIN, CONF_RMT_SYMBOLS, CONF_USE_DMA, + PlatformFramework, ) from esphome.core import CORE @@ -95,3 +97,19 @@ async def to_code(config): await automation.build_automation( 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, + }, + } +) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp new file mode 100644 index 0000000000..a6eac58dc6 --- /dev/null +++ b/esphome/components/rp2040/helpers.cpp @@ -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 +#endif +#include +#include + +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 diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ce23c1f800..dd8635f0c0 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -118,7 +118,7 @@ optional QuantileFilter::new_value(float value) { size_t queue_size = quantile_queue.size(); if (queue_size) { 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]; } } diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 26031a8da5..e085a09eac 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -40,3 +41,18 @@ async def to_code(config): elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") 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 diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 55a4b9c8f6..58bfc3f411 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_CLK_PIN, @@ -31,6 +32,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority 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)}, 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}, + } +) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 0547a77184..1f039cff78 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -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_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() { this->do_update_(); diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py new file mode 100644 index 0000000000..492febe283 --- /dev/null +++ b/esphome/components/sx126x/__init__.py @@ -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 diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h new file mode 100644 index 0000000000..520ef99718 --- /dev/null +++ b/esphome/components/sx126x/automation.h @@ -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 class RunImageCalAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->run_image_cal(); } +}; + +template class SendPacketAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + + void set_data_static(const std::vector &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(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +template class SetModeTxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_tx(); } +}; + +template class SetModeRxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_rx(); } +}; + +template class SetModeSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_sleep(); } +}; + +template class SetModeStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); } +}; + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/packet_transport/__init__.py b/esphome/components/sx126x/packet_transport/__init__.py new file mode 100644 index 0000000000..4d79b23ac1 --- /dev/null +++ b/esphome/components/sx126x/packet_transport/__init__.py @@ -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)) diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp new file mode 100644 index 0000000000..2cfc4b700e --- /dev/null +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp @@ -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 &buf) const { this->parent_->transmit_packet(buf); } + +void SX126xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); } + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.h b/esphome/components/sx126x/packet_transport/sx126x_transport.h new file mode 100644 index 0000000000..755d30417d --- /dev/null +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.h @@ -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 + +namespace esphome { +namespace sx126x { + +class SX126xTransport : public packet_transport::PacketTransport, public Parented, public SX126xListener { + public: + void setup() override; + void update() override; + void on_packet(const std::vector &packet, float rssi, float snr) override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + void send_packet(const std::vector &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 diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp new file mode 100644 index 0000000000..b1c81b324a --- /dev/null +++ b/esphome/components/sx126x/sx126x.cpp @@ -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 &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 &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 &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 &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 diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h new file mode 100644 index 0000000000..fd5c37942d --- /dev/null +++ b/esphome/components/sx126x/sx126x.h @@ -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 +#include + +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 &packet, float rssi, float snr) = 0; +}; + +class SX126x : public Component, + public spi::SPIDevice { + 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 &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 &packet); + void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); } + Trigger, 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 &packet); + void write_fifo_(uint8_t offset, const std::vector &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 &packet, float rssi, float snr); + void wait_busy_(); + Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + std::vector listeners_; + std::vector packet_; + std::vector 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 diff --git a/esphome/components/sx126x/sx126x_reg.h b/esphome/components/sx126x/sx126x_reg.h new file mode 100644 index 0000000000..3b12d822b5 --- /dev/null +++ b/esphome/components/sx126x/sx126x_reg.h @@ -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 diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 9d2cda549b..e322a6951d 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -21,10 +21,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { void Syslog::setup() { 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_) return; // 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; auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S"); - unsigned len = strlen(message); + size_t len = message_len; // remove color formatting if (this->strip_ && message[0] == 0x1B && len > 11) { message += 7; diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index 421a9bee73..e3b2f7dae5 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -17,7 +17,7 @@ class Syslog : public Component, public Parented { protected: 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_; bool strip_{true}; int facility_{16}; diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a0908a299c..7d4c6360fe 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -2,6 +2,7 @@ import re from esphome import automation, pins import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AFTER, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_TX_PIN, CONF_UART_ID, PLATFORM_HOST, + PlatformFramework, ) from esphome.core import CORE import esphome.final_validate as fv @@ -438,3 +440,19 @@ async def uart_write_to_code(config, action_id, template_arg, args): else: cg.add(var.set_data_static(data)) 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, + }, + } +) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 575234e780..75c6b84b79 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() { } else { // Partial out (PTOUT), makes the display exit partial mode 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); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 20ff1a7c29..8ced5b7e18 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -287,7 +287,8 @@ void WebServer::setup() { if (logger::global_logger != nullptr && this->expose_log_) { logger::global_logger->add_on_log_callback( // 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()); }); } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 0c15881d1e..ef1b03a73b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -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 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 - 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. */ #ifdef USE_ARDUINO diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e8ae9b1b4e..61f37556ba 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -3,6 +3,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.network import IPAddress +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AP, @@ -39,6 +40,7 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + PlatformFramework, ) from esphome.core import CORE, HexInt, coroutine_with_priority 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 cg.register_component(var, config) 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}, + } +) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 54242bc259..50ce4e8e34 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -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: @@ -103,3 +119,60 @@ def merge_config(full_old, full_new): return 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") diff --git a/esphome/const.py b/esphome/const.py index 4aeb5179e6..a30df6ef35 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,20 +1,65 @@ """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-_" VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) -PLATFORM_BK72XX = "bk72xx" -PLATFORM_ESP32 = "esp32" -PLATFORM_ESP8266 = "esp8266" -PLATFORM_HOST = "host" -PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" -PLATFORM_LN882X = "ln882x" -PLATFORM_RP2040 = "rp2040" -PLATFORM_RTL87XX = "rtl87xx" + +class Platform(StrEnum): + """Platform identifiers for ESPHome.""" + + BK72XX = "bk72xx" + ESP32 = "esp32" + ESP8266 = "esp8266" + 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"} diff --git a/esphome/core/application.h b/esphome/core/application.h index 6ee05309ca..f2b5cb5c89 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -368,6 +368,17 @@ class Application { 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 const std::vector &get_devices() { return this->devices_; } #endif @@ -376,218 +387,92 @@ class Application { #endif #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } - binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->binary_sensors_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors) #endif #ifdef USE_SWITCH const std::vector &get_switches() { return this->switches_; } - switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->switches_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(switch_::Switch, switch, switches) #endif #ifdef USE_BUTTON const std::vector &get_buttons() { return this->buttons_; } - button::Button *get_button_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->buttons_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(button::Button, button, buttons) #endif #ifdef USE_SENSOR const std::vector &get_sensors() { return this->sensors_; } - sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->sensors_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors) #endif #ifdef USE_TEXT_SENSOR const std::vector &get_text_sensors() { return this->text_sensors_; } - text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->text_sensors_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors) #endif #ifdef USE_FAN const std::vector &get_fans() { return this->fans_; } - fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->fans_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(fan::Fan, fan, fans) #endif #ifdef USE_COVER const std::vector &get_covers() { return this->covers_; } - cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->covers_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(cover::Cover, cover, covers) #endif #ifdef USE_LIGHT const std::vector &get_lights() { return this->lights_; } - light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->lights_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(light::LightState, light, lights) #endif #ifdef USE_CLIMATE const std::vector &get_climates() { return this->climates_; } - climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->climates_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(climate::Climate, climate, climates) #endif #ifdef USE_NUMBER const std::vector &get_numbers() { return this->numbers_; } - number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->numbers_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(number::Number, number, numbers) #endif #ifdef USE_DATETIME_DATE const std::vector &get_dates() { return this->dates_; } - datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->dates_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(datetime::DateEntity, date, dates) #endif #ifdef USE_DATETIME_TIME const std::vector &get_times() { return this->times_; } - datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->times_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(datetime::TimeEntity, time, times) #endif #ifdef USE_DATETIME_DATETIME const std::vector &get_datetimes() { return this->datetimes_; } - datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->datetimes_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes) #endif #ifdef USE_TEXT const std::vector &get_texts() { return this->texts_; } - text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->texts_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(text::Text, text, texts) #endif #ifdef USE_SELECT const std::vector &get_selects() { return this->selects_; } - select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->selects_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(select::Select, select, selects) #endif #ifdef USE_LOCK const std::vector &get_locks() { return this->locks_; } - lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->locks_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(lock::Lock, lock, locks) #endif #ifdef USE_VALVE const std::vector &get_valves() { return this->valves_; } - valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->valves_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(valve::Valve, valve, valves) #endif #ifdef USE_MEDIA_PLAYER const std::vector &get_media_players() { return this->media_players_; } - media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->media_players_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players) #endif #ifdef USE_ALARM_CONTROL_PANEL const std::vector &get_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) { - for (auto *obj : this->alarm_control_panels_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels) #endif #ifdef USE_EVENT const std::vector &get_events() { return this->events_; } - event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->events_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(event::Event, event, events) #endif #ifdef USE_UPDATE const std::vector &get_updates() { return this->updates_; } - update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->updates_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(update::UpdateEntity, update, updates) #endif Scheduler scheduler; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 9ef30081aa..9d863e56cd 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -26,17 +26,17 @@ static const char *const TAG = "component"; // 1. Components are never destroyed in ESPHome // 2. Failed components remain failed (no recovery mechanism) // 3. Memory usage is minimal (only failures with custom messages are stored) -static std::unique_ptr>> &get_component_error_messages() { - static std::unique_ptr>> 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>> component_error_messages; // Setup priority overrides - freed after setup completes -// Typically < 5 entries, lazy allocated -static std::unique_ptr>> &get_setup_priority_overrides() { - static std::unique_ptr>> instance; - return instance; -} +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::unique_ptr>> setup_priority_overrides; +} // namespace namespace setup_priority { @@ -130,8 +130,8 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = "unspecified"; - if (get_component_error_messages()) { - for (const auto &pair : *get_component_error_messages()) { + if (component_error_messages) { + for (const auto &pair : *component_error_messages) { if (pair.first == this) { error_msg = pair.second; 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); if (strcmp(message, "unspecified") != 0) { // Lazy allocate the error messages vector if needed - if (!get_component_error_messages()) { - get_component_error_messages() = std::make_unique>>(); + if (!component_error_messages) { + component_error_messages = std::make_unique>>(); } // 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) { pair.second = message; return; } } // Add new error message - get_component_error_messages()->emplace_back(this, message); + component_error_messages->emplace_back(this, message); } } 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() {} float Component::get_actual_setup_priority() const { // 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) - for (const auto &pair : *get_setup_priority_overrides()) { + for (const auto &pair : *setup_priority_overrides) { if (pair.first == this) { return pair.second; } @@ -334,14 +334,14 @@ float Component::get_actual_setup_priority() const { } void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed - if (!get_setup_priority_overrides()) { - get_setup_priority_overrides() = std::make_unique>>(); + if (!setup_priority_overrides) { + setup_priority_overrides = std::make_unique>>(); // 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 - for (auto &pair : *get_setup_priority_overrides()) { + for (auto &pair : *setup_priority_overrides) { if (pair.first == this) { pair.second = priority; return; @@ -349,7 +349,7 @@ void Component::set_setup_priority(float priority) { } // Add new override - get_setup_priority_overrides()->emplace_back(this, priority); + setup_priority_overrides->emplace_back(this, priority); } bool Component::has_overridden_loop() const { @@ -414,7 +414,7 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} void clear_setup_priority_overrides() { // Free the setup priority map completely - get_setup_priority_overrides().reset(); + setup_priority_overrides.reset(); } } // namespace esphome diff --git a/esphome/core/config.py b/esphome/core/config.py index 641c73a292..f73369f28f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -6,6 +6,7 @@ from pathlib import Path from esphome import automation, core import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AREA, @@ -35,6 +36,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VERSION, KEY_CORE, + PlatformFramework, __version__ as ESPHOME_VERSION, ) 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(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 + } +) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 4115b97391..d73009436b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -33,6 +33,7 @@ #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY +#define USE_ENTITY_ICON #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT #define USE_FAN diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 6afd02ff65..2ea9c77a3e 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -27,12 +27,22 @@ void EntityBase::set_name(const char *name) { // Entity Icon std::string EntityBase::get_icon() const { +#ifdef USE_ENTITY_ICON if (this->icon_c_str_ == nullptr) { return ""; } 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 std::string EntityBase::get_object_id() const { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4819b66108..00b1264ed0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -80,7 +80,9 @@ class EntityBase { StringRef name_; const char *object_id_c_str_{nullptr}; +#ifdef USE_ENTITY_ICON const char *icon_c_str_{nullptr}; +#endif uint32_t object_id_hash_{}; #ifdef USE_DEVICES Device *device_{}; diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 2442fbca4b..a3244856a2 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,6 +1,7 @@ from collections.abc import Callable import logging +import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_ID, @@ -108,6 +109,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: + # Add USE_ENTITY_ICON define when icons are used + cg.add_define("USE_ENTITY_ICON") add(var.set_icon(config[CONF_ICON])) if CONF_ENTITY_CATEGORY in config: add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 7d9b86fccd..b46077af02 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -12,47 +12,10 @@ #include #include -#ifdef USE_HOST -#ifndef _WIN32 -#include -#include -#include -#endif -#include -#endif -#if defined(USE_ESP8266) -#include -#include -// for xt_rsil()/xt_wsr_ps() -#include -#elif defined(USE_ESP32_FRAMEWORK_ARDUINO) -#include -#elif defined(USE_ESP_IDF) -#include -#include -#include "esp_random.h" -#include "esp_system.h" -#elif defined(USE_RP2040) -#if defined(USE_WIFI) -#include -#endif -#include -#include -#elif defined(USE_HOST) -#include -#include -#endif #ifdef USE_ESP32 -#include "esp_efuse.h" -#include "esp_efuse_table.h" -#include "esp_mac.h" #include "rom/crc.h" #endif -#ifdef USE_LIBRETINY -#include // for macAddress() -#endif - namespace esphome { static const char *const TAG = "helpers"; @@ -177,70 +140,7 @@ uint32_t fnv1_hash(const std::string &str) { return hash; } -#ifdef USE_ESP32 -uint32_t random_uint32() { return esp_random(); } -#elif defined(USE_ESP8266) -uint32_t random_uint32() { return os_random(); } -#elif defined(USE_RP2040) -uint32_t random_uint32() { - uint32_t result = 0; - for (uint8_t i = 0; i < 32; i++) { - result <<= 1; - result |= rosc_hw->randombit; - } - return result; -} -#elif defined(USE_LIBRETINY) -uint32_t random_uint32() { return rand(); } -#elif defined(USE_HOST) -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution dist(0, std::numeric_limits::max()); - return dist(rng); -} -#endif float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } -#ifdef USE_ESP32 -bool random_bytes(uint8_t *data, size_t len) { - esp_fill_random(data, len); - return true; -} -#elif defined(USE_ESP8266) -bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } -#elif defined(USE_RP2040) -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; -} -#elif defined(USE_LIBRETINY) -bool random_bytes(uint8_t *data, size_t len) { - lt_rand_bytes(data, len); - return true; -} -#elif defined(USE_HOST) -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; -} -#endif // Strings @@ -358,53 +258,60 @@ std::string format_hex(const uint8_t *data, size_t length) { std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } -std::string format_hex_pretty(const uint8_t *data, size_t length) { - if (length == 0) +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { + if (data == nullptr || length == 0) return ""; std::string ret; - ret.resize(3 * length - 1); + uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise + ret.resize(multiple * length - (separator ? 1 : 0)); for (size_t i = 0; i < length; i++) { - ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); - ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F); - if (i != length - 1) - ret[3 * i + 2] = '.'; + ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); + ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); + if (separator && i != length - 1) + ret[multiple * i + 2] = separator; } - if (length > 4) - return ret + " (" + to_string(length) + ")"; + if (show_length && length > 4) + return ret + " (" + std::to_string(length) + ")"; return ret; } -std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } +std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { + return format_hex_pretty(data.data(), data.size(), separator, show_length); +} -std::string format_hex_pretty(const uint16_t *data, size_t length) { - if (length == 0) +std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) { + if (data == nullptr || length == 0) return ""; std::string ret; - ret.resize(5 * length - 1); + uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise + ret.resize(multiple * length - (separator ? 1 : 0)); for (size_t i = 0; i < length; i++) { - ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); - ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); - ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); - ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F); - if (i != length - 1) - ret[5 * i + 2] = '.'; + ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); + ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); + ret[multiple * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); + ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F); + if (separator && i != length - 1) + ret[multiple * i + 4] = separator; } - if (length > 4) - return ret + " (" + to_string(length) + ")"; + if (show_length && length > 4) + return ret + " (" + std::to_string(length) + ")"; return ret; } -std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } -std::string format_hex_pretty(const std::string &data) { +std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { + return format_hex_pretty(data.data(), data.size(), separator, show_length); +} +std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { if (data.empty()) return ""; std::string ret; - ret.resize(3 * data.length() - 1); + uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise + ret.resize(multiple * data.length() - (separator ? 1 : 0)); for (size_t i = 0; i < data.length(); i++) { - ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); - ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F); - if (i != data.length() - 1) - ret[3 * i + 2] = '.'; + ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); + ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); + if (separator && i != data.length() - 1) + ret[multiple * i + 2] = separator; } - if (data.length() > 4) + if (show_length && data.length() > 4) return ret + " (" + std::to_string(data.length()) + ")"; return ret; } @@ -460,9 +367,22 @@ int8_t step_to_accuracy_decimals(float step) { return str.length() - dot_pos - 1; } -static const std::string BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; +// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes) +static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +// Helper function to find the index of a base64 character in the lookup table. +// Returns the character's position (0-63) if found, or 0 if not found. +// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters. +// This is safe because is_base64() is ALWAYS checked before calling this function, +// preventing invalid characters from ever reaching here. The base64_decode function +// stops processing at the first invalid character due to the is_base64() check in its +// while loop condition, making this edge case harmless in practice. +static inline uint8_t base64_find_char(char c) { + const char *pos = strchr(BASE64_CHARS, c); + return pos ? (pos - BASE64_CHARS) : 0; +} static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); } @@ -484,7 +404,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) { char_array_4[3] = char_array_3[2] & 0x3f; for (i = 0; (i < 4); i++) - ret += BASE64_CHARS[char_array_4[i]]; + ret += BASE64_CHARS[static_cast(char_array_4[i])]; i = 0; } } @@ -499,7 +419,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) { char_array_4[3] = char_array_3[2] & 0x3f; for (j = 0; (j < i + 1); j++) - ret += BASE64_CHARS[char_array_4[j]]; + ret += BASE64_CHARS[static_cast(char_array_4[j])]; while ((i++ < 3)) ret += '='; @@ -526,12 +446,15 @@ std::vector base64_decode(const std::string &encoded_string) { uint8_t char_array_4[4], char_array_3[3]; std::vector ret; + // SAFETY: The loop condition checks is_base64() before processing each character. + // This ensures base64_find_char() is only called on valid base64 characters, + // preventing the edge case where invalid chars would return 0 (same as 'A'). while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) { char_array_4[i++] = encoded_string[in]; in++; if (i == 4) { for (i = 0; i < 4; i++) - char_array_4[i] = BASE64_CHARS.find(char_array_4[i]); + char_array_4[i] = base64_find_char(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); @@ -548,7 +471,7 @@ std::vector base64_decode(const std::string &encoded_string) { char_array_4[j] = 0; for (j = 0; j < 4; j++) - char_array_4[j] = BASE64_CHARS.find(char_array_4[j]); + char_array_4[j] = base64_find_char(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); @@ -644,42 +567,6 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green blue += delta; } -// System APIs -#if defined(USE_ESP8266) || defined(USE_RP2040) -// 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() {} -#elif defined(USE_ESP32) || defined(USE_LIBRETINY) -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_); } -#elif defined(USE_HOST) -// Host platform uses std::mutex for proper thread synchronization -Mutex::Mutex() { handle_ = new std::mutex(); } -Mutex::~Mutex() { delete static_cast(handle_); } -void Mutex::lock() { static_cast(handle_)->lock(); } -bool Mutex::try_lock() { return static_cast(handle_)->try_lock(); } -void Mutex::unlock() { static_cast(handle_)->unlock(); } -#endif - -#if defined(USE_ESP8266) -IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } -IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } -#elif defined(USE_ESP32) || defined(USE_LIBRETINY) -// 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(); } -#elif defined(USE_RP2040) -IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } -IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } -#endif - uint8_t HighFrequencyLoopRequester::num_requests = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void HighFrequencyLoopRequester::start() { if (this->started_) @@ -695,45 +582,6 @@ void HighFrequencyLoopRequester::stop() { } bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } -#if defined(USE_HOST) -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)); -} -#elif defined(USE_ESP32) -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 -} -#elif defined(USE_ESP8266) -void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) - wifi_get_macaddr(STATION_IF, mac); -} -#elif defined(USE_RP2040) -void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) -#ifdef USE_WIFI - WiFi.macAddress(mac); -#endif -} -#elif defined(USE_LIBRETINY) -void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) - WiFi.macAddress(mac); -} -#endif - std::string get_mac_address() { uint8_t mac[6]; get_mac_address_raw(mac); @@ -746,24 +594,10 @@ std::string get_mac_address_pretty() { return format_mac_address_pretty(mac); } -#ifdef USE_ESP32 -void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } +#ifndef USE_ESP32 +bool has_custom_mac_address() { return false; } #endif -bool has_custom_mac_address() { -#if defined(USE_ESP32) && !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 -} - bool mac_address_is_valid(const uint8_t *mac) { bool is_all_zeros = true; bool is_all_ones = true; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d92cf07702..58f162ff9d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -344,20 +344,149 @@ template std::string format_hex(const std::array &dat return format_hex(data.data(), data.size()); } -/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex. -std::string format_hex_pretty(const uint8_t *data, size_t length); -/// Format the word array \p data of length \p len in pretty-printed, human-readable hex. -std::string format_hex_pretty(const uint16_t *data, size_t length); -/// Format the vector \p data in pretty-printed, human-readable hex. -std::string format_hex_pretty(const std::vector &data); -/// Format the vector \p data in pretty-printed, human-readable hex. -std::string format_hex_pretty(const std::vector &data); -/// Format the string \p data in pretty-printed, human-readable hex. -std::string format_hex_pretty(const std::string &data); -/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. -template::value, int> = 0> std::string format_hex_pretty(T val) { +/** Format a byte array in pretty-printed, human-readable hex format. + * + * Converts binary data to a hexadecimal string representation with customizable formatting. + * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator. + * Optionally includes the total byte count in parentheses at the end. + * + * @param data Pointer to the byte array to format. + * @param length Number of bytes in the array. + * @param separator Character to use between hex bytes (default: '.'). + * @param show_length Whether to append the byte count in parentheses (default: true). + * @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters. + * + * @note Returns empty string if data is nullptr or length is 0. + * @note The length will only be appended if show_length is true AND the length is greater than 4. + * + * Example: + * @code + * uint8_t data[] = {0xA1, 0xB2, 0xC3}; + * format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts) + * uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5}; + * format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)" + * format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)" + * format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5" + * @endcode + */ +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true); + +/** Format a 16-bit word array in pretty-printed, human-readable hex format. + * + * Similar to the byte array version, but formats 16-bit words as 4-digit hex values. + * + * @param data Pointer to the 16-bit word array to format. + * @param length Number of 16-bit words in the array. + * @param separator Character to use between hex words (default: '.'). + * @param show_length Whether to append the word count in parentheses (default: true). + * @return Formatted hex string with 4-digit hex values per word. + * + * @note The length will only be appended if show_length is true AND the length is greater than 4. + * + * Example: + * @code + * uint16_t data[] = {0xA1B2, 0xC3D4}; + * format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts) + * uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6}; + * format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)" + * @endcode + */ +std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true); + +/** Format a byte vector in pretty-printed, human-readable hex format. + * + * Convenience overload for std::vector. Formats each byte as a two-digit + * uppercase hex value with customizable separator. + * + * @param data Vector of bytes to format. + * @param separator Character to use between hex bytes (default: '.'). + * @param show_length Whether to append the byte count in parentheses (default: true). + * @return Formatted hex string representation of the vector contents. + * + * @note The length will only be appended if show_length is true AND the vector size is greater than 4. + * + * Example: + * @code + * std::vector data = {0xDE, 0xAD, 0xBE, 0xEF}; + * format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts) + * std::vector data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA}; + * format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)" + * format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)" + * @endcode + */ +std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true); + +/** Format a 16-bit word vector in pretty-printed, human-readable hex format. + * + * Convenience overload for std::vector. Each 16-bit word is formatted + * as a 4-digit uppercase hex value in big-endian order. + * + * @param data Vector of 16-bit words to format. + * @param separator Character to use between hex words (default: '.'). + * @param show_length Whether to append the word count in parentheses (default: true). + * @return Formatted hex string representation of the vector contents. + * + * @note The length will only be appended if show_length is true AND the vector size is greater than 4. + * + * Example: + * @code + * std::vector data = {0x1234, 0x5678}; + * format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts) + * std::vector data2 = {0x1234, 0x5678, 0x9ABC}; + * format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)" + * @endcode + */ +std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true); + +/** Format a string's bytes in pretty-printed, human-readable hex format. + * + * Treats each character in the string as a byte and formats it in hex. + * Useful for debugging binary data stored in std::string containers. + * + * @param data String whose bytes should be formatted as hex. + * @param separator Character to use between hex bytes (default: '.'). + * @param show_length Whether to append the byte count in parentheses (default: true). + * @return Formatted hex string representation of the string's byte contents. + * + * @note The length will only be appended if show_length is true AND the string length is greater than 4. + * + * Example: + * @code + * std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43 + * format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts) + * std::string data2 = "ABCDE"; + * format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)" + * @endcode + */ +std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true); + +/** Format an unsigned integer in pretty-printed, human-readable hex format. + * + * Converts the integer to big-endian byte order and formats each byte as hex. + * The most significant byte appears first in the output string. + * + * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.). + * @param val The unsigned integer value to format. + * @param separator Character to use between hex bytes (default: '.'). + * @param show_length Whether to append the byte count in parentheses (default: true). + * @return Formatted hex string with most significant byte first. + * + * @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4. + * + * Example: + * @code + * uint32_t value = 0x12345678; + * format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts) + * uint64_t value2 = 0x123456789ABCDEF0; + * format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)" + * format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)" + * format_hex_pretty(0x1234); // Returns "12.34" + * @endcode + */ +template::value, int> = 0> +std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) { val = convert_big_endian(val); - return format_hex_pretty(reinterpret_cast(&val), sizeof(T)); + return format_hex_pretty(reinterpret_cast(&val), sizeof(T), separator, show_length); } /// Format the byte array \p data of length \p len in binary. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 515f6fd355..d3da003a88 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -62,16 +62,16 @@ static void validate_static_string(const char *name) { void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, uint32_t delay, std::function func) { // Get the name as const char* - const char *name_cstr = - is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); - // Cancel existing timer if name is not empty - if (name_cstr != nullptr && name_cstr[0] != '\0') { - this->cancel_item_(component, name_cstr, type); - } - - if (delay == SCHEDULER_DONT_RUN) + if (delay == SCHEDULER_DONT_RUN) { + // Still need to cancel existing timer if name is not empty + if (this->is_name_valid_(name_cstr)) { + LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); + } return; + } // Create and populate the scheduler item auto item = make_unique(); @@ -87,6 +87,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); this->defer_queue_.push_back(std::move(item)); return; } @@ -122,7 +123,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif - this->push_(std::move(item)); + LockGuard guard{this->lock_}; + // If name is provided, do atomic cancel-and-add + if (this->is_name_valid_(name_cstr)) { + // Cancel existing items + this->cancel_item_locked_(component, name_cstr, type); + } + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(item)); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { @@ -134,10 +143,10 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); } bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { - return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); + return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT); } bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { - return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); + return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, std::function func) { @@ -149,10 +158,10 @@ void HOT Scheduler::set_interval(Component *component, const char *name, uint32_ this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { - return this->cancel_item_(component, name, SchedulerItem::INTERVAL); + return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL); } bool HOT Scheduler::cancel_interval(Component *component, const char *name) { - return this->cancel_item_(component, name, SchedulerItem::INTERVAL); + return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL); } struct RetryArgs { @@ -211,6 +220,9 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) } optional HOT Scheduler::next_schedule_in() { + // IMPORTANT: This method should only be called from the main thread (loop task). + // It calls empty_() and accesses items_[0] without holding a lock, which is only + // safe when called from the main thread. Other threads must not call this method. if (this->empty_()) return {}; auto &item = this->items_[0]; @@ -230,6 +242,10 @@ void HOT Scheduler::call() { // - No deferred items exist in to_add_, so processing order doesn't affect correctness // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach // (ESP8266: single-core, RP2040: empty mutex implementation). + // + // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still + // processed here. They are removed from the queue normally via pop_front() but skipped + // during execution by should_skip_item_(). This is intentional - no memory leak occurs. while (!this->defer_queue_.empty()) { // The outer check is done without a lock for performance. If the queue // appears non-empty, we lock and process an item. We don't need to check @@ -261,10 +277,12 @@ void HOT Scheduler::call() { ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, this->last_millis_); while (!this->empty_()) { - this->lock_.lock(); - auto item = std::move(this->items_[0]); - this->pop_raw_(); - this->lock_.unlock(); + std::unique_ptr item; + { + LockGuard guard{this->lock_}; + item = std::move(this->items_[0]); + this->pop_raw_(); + } const char *name = item->get_name(); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, @@ -278,33 +296,35 @@ void HOT Scheduler::call() { { LockGuard guard{this->lock_}; this->items_ = std::move(old_items); + // Rebuild heap after moving items back + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } } #endif // ESPHOME_DEBUG_SCHEDULER - auto to_remove_was = to_remove_; - auto items_was = this->items_.size(); // If we have too many items to remove - if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + // We hold the lock for the entire cleanup operation because: + // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout + // 2. Other threads must see either the old state or the new state, not intermediate states + // 3. The operation is already expensive (O(n)), so lock overhead is negligible + // 4. No operations inside can block or take other locks, so no deadlock risk + LockGuard guard{this->lock_}; + std::vector> valid_items; - while (!this->empty_()) { - LockGuard guard{this->lock_}; - auto item = std::move(this->items_[0]); - this->pop_raw_(); - valid_items.push_back(std::move(item)); + + // Move all non-removed items to valid_items + for (auto &item : this->items_) { + if (!item->remove) { + valid_items.push_back(std::move(item)); + } } - { - LockGuard guard{this->lock_}; - this->items_ = std::move(valid_items); - } - - // The following should not happen unless I'm missing something - if (to_remove_ != 0) { - ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this", - to_remove_was, to_remove_, items_was, items_.size()); - to_remove_ = 0; - } + // Replace items_ with the filtered list + this->items_ = std::move(valid_items); + // Rebuild the heap structure since items are no longer in heap order + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + this->to_remove_ = 0; } while (!this->empty_()) { @@ -336,26 +356,25 @@ void HOT Scheduler::call() { } { - this->lock_.lock(); + LockGuard guard{this->lock_}; // new scope, item from before might have been moved in the vector auto item = std::move(this->items_[0]); - // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. this->pop_raw_(); - this->lock_.unlock(); - if (item->remove) { // We were removed/cancelled in the function call, stop - to_remove_--; + this->to_remove_--; continue; } if (item->type == SchedulerItem::INTERVAL) { item->next_execution_ = now + item->interval; - this->push_(std::move(item)); + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(item)); } } } @@ -375,36 +394,37 @@ void HOT Scheduler::process_to_add() { this->to_add_.clear(); } void HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just return + // Reading to_remove_ without lock is safe because: + // 1. We only call this from the main thread during call() + // 2. If it's 0, there's definitely nothing to cleanup + // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration + // 4. Not all platforms support atomics, so we accept this race in favor of performance + // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless + if (this->to_remove_ == 0) + return; + + // We must hold the lock for the entire cleanup operation because: + // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 2. We're decrementing to_remove_ which is also modified by other threads + // (though all modifications are already under lock) + // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() + // 4. We need a consistent view of items_ and to_remove_ throughout the operation + // Without the lock, we could access items_ while another thread is reading it, + // leading to race conditions + LockGuard guard{this->lock_}; while (!this->items_.empty()) { auto &item = this->items_[0]; if (!item->remove) return; - - to_remove_--; - - { - LockGuard guard{this->lock_}; - this->pop_raw_(); - } + this->to_remove_--; + this->pop_raw_(); } } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->items_.pop_back(); } -void HOT Scheduler::push_(std::unique_ptr item) { - LockGuard guard{this->lock_}; - this->to_add_.push_back(std::move(item)); -} -// Helper function to check if item matches criteria for cancellation -bool HOT Scheduler::matches_item_(const std::unique_ptr &item, Component *component, - const char *name_cstr, SchedulerItem::Type type) { - if (item->component != component || item->type != type || item->remove) { - return false; - } - const char *item_name = item->get_name(); - return item_name != nullptr && strcmp(name_cstr, item_name) == 0; -} // Helper to execute a scheduler item void HOT Scheduler::execute_item_(SchedulerItem *item) { @@ -417,55 +437,56 @@ void HOT Scheduler::execute_item_(SchedulerItem *item) { } // Common implementation for cancel operations -bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, - SchedulerItem::Type type) { +bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr, + SchedulerItem::Type type) { // Get the name as const char* - const char *name_cstr = - is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); // Handle null or empty names - if (name_cstr == nullptr) + if (!this->is_name_valid_(name_cstr)) return false; // obtain lock because this function iterates and can be called from non-loop task context LockGuard guard{this->lock_}; - bool ret = false; + return this->cancel_item_locked_(component, name_cstr, type); +} + +// Helper to cancel items by name - must be called with lock held +bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) { + size_t total_cancelled = 0; // Check all containers for matching items #if !defined(USE_ESP8266) && !defined(USE_RP2040) - // Only check defer_queue_ on platforms that have it - for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type)) { - item->remove = true; - ret = true; + // Only check defer queue for timeouts (intervals never go there) + if (type == SchedulerItem::TIMEOUT) { + for (auto &item : this->defer_queue_) { + if (this->matches_item_(item, component, name_cstr, type)) { + item->remove = true; + total_cancelled++; + } } } #endif + // Cancel items in the main heap for (auto &item : this->items_) { if (this->matches_item_(item, component, name_cstr, type)) { item->remove = true; - ret = true; - this->to_remove_++; // Only track removals for heap items + total_cancelled++; + this->to_remove_++; // Track removals for heap items } } + // Cancel items in to_add_ for (auto &item : this->to_add_) { if (this->matches_item_(item, component, name_cstr, type)) { item->remove = true; - ret = true; + total_cancelled++; + // Don't track removals for to_add_ items } } - return ret; -} - -bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { - return this->cancel_item_common_(component, false, &name, type); -} - -bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) { - return this->cancel_item_common_(component, true, name, type); + return total_cancelled > 0; } uint64_t Scheduler::millis_() { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bf5e63cccf..084ff699c5 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "esphome/core/component.h" @@ -98,9 +99,9 @@ class Scheduler { SchedulerItem(const SchedulerItem &) = delete; SchedulerItem &operator=(const SchedulerItem &) = delete; - // Default move operations - SchedulerItem(SchedulerItem &&) = default; - SchedulerItem &operator=(SchedulerItem &&) = default; + // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly + SchedulerItem(SchedulerItem &&) = delete; + SchedulerItem &operator=(SchedulerItem &&) = delete; // Helper to get the name regardless of storage type const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } @@ -139,17 +140,42 @@ class Scheduler { uint64_t millis_(); void cleanup_(); void pop_raw_(); - void push_(std::unique_ptr item); - // Common implementation for cancel operations - bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); private: - bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); - bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type); + // Helper to cancel items by name - must be called with lock held + bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type); - // Helper functions for cancel operations - bool matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type); + // Helper to extract name as const char* from either static string or std::string + inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { + return is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + } + + // Helper to check if a name is valid (not null and not empty) + inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; } + + // Common implementation for cancel operations + bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + + // Helper function to check if item matches criteria for cancellation + inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, + SchedulerItem::Type type) { + if (item->component != component || item->type != type || item->remove) { + return false; + } + const char *item_name = item->get_name(); + if (item_name == nullptr) { + return false; + } + // Fast path: if pointers are equal + // This is effective because the core ESPHome codebase uses static strings (const char*) + // for component names. The std::string overloads exist only for compatibility with + // external components, but are rarely used in practice. + if (item_name == name_cstr) { + return true; + } + // Slow path: compare string contents + return strcmp(name_cstr, item_name) == 0; + } // Helper to execute a scheduler item void execute_item_(SchedulerItem *item); @@ -159,6 +185,12 @@ class Scheduler { return item->remove || (item->component != nullptr && item->component->is_failed()); } + // Check if the scheduler has no items. + // IMPORTANT: This method should only be called from the main thread (loop task). + // It performs cleanup of removed items and checks if the queue is empty. + // The items_.empty() check at the end is done without a lock for performance, + // which is safe because this is only called from the main thread while other + // threads only add items (never remove them). bool empty_() { this->cleanup_(); return this->items_.empty(); diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index e4825298f7..b138cfd272 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -9,6 +9,7 @@ import os from typing import TYPE_CHECKING, Any from esphome import const, util +from esphome.enum import StrEnum from esphome.storage_json import StorageJSON, ext_storage_path from .const import ( @@ -18,7 +19,6 @@ from .const import ( EVENT_ENTRY_STATE_CHANGED, EVENT_ENTRY_UPDATED, ) -from .enum import StrEnum from .util.subprocess import async_run_system_command if TYPE_CHECKING: diff --git a/esphome/dashboard/enum.py b/esphome/enum.py similarity index 100% rename from esphome/dashboard/enum.py rename to esphome/enum.py diff --git a/esphome/loader.py b/esphome/loader.py index 79a1d7f576..7b2472521a 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -112,8 +112,17 @@ class ComponentManifest: This will return all cpp source files that are located in the same folder as the loaded .py file (does not look through subdirectories) """ - ret = [] + ret: list[FileResource] = [] + # Get filter function for source files + filter_source_files_func = getattr(self.module, "FILTER_SOURCE_FILES", None) + + # Get list of files to exclude + excluded_files = ( + set(filter_source_files_func()) if filter_source_files_func else set() + ) + + # Process all resources for resource in ( r.name for r in importlib.resources.files(self.package).iterdir() @@ -124,6 +133,11 @@ class ComponentManifest: if not importlib.resources.files(self.package).joinpath(resource).is_file(): # Not a resource = this is a directory (yeah this is confusing) continue + + # Skip excluded files + if resource in excluded_files: + continue + ret.append(FileResource(self.package, resource)) return ret diff --git a/requirements.txt b/requirements.txt index a6bcebaeea..d056f22e28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==34.1.0 +aioesphomeapi==34.2.0 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2266dda81c..df1f3f8caa 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -290,7 +290,7 @@ class DoubleType(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%g", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n' o += "out.append(buffer);" return o @@ -312,7 +312,7 @@ class FloatType(TypeInfo): wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%g", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n' o += "out.append(buffer);" return o @@ -334,7 +334,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' o += "out.append(buffer);" return o @@ -356,7 +356,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' o += "out.append(buffer);" return o @@ -378,7 +378,7 @@ class Int32Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRId32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -400,7 +400,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' o += "out.append(buffer);" return o @@ -422,7 +422,7 @@ class Fixed32Type(TypeInfo): wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRIu32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n' o += "out.append(buffer);" return o @@ -555,7 +555,7 @@ class UInt32Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRIu32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n' o += "out.append(buffer);" return o @@ -607,7 +607,7 @@ class SFixed32Type(TypeInfo): wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRId32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -629,7 +629,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' o += "out.append(buffer);" return o @@ -651,7 +651,7 @@ class SInt32Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRId32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -673,7 +673,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' o += "out.append(buffer);" return o diff --git a/script/ci-custom.py b/script/ci-custom.py index fbabbc1e74..d0b518251f 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -559,6 +559,12 @@ def lint_relative_py_import(fname): "esphome/components/libretiny/core.cpp", "esphome/components/host/core.cpp", "esphome/components/zephyr/core.cpp", + "esphome/components/esp32/helpers.cpp", + "esphome/components/esp8266/helpers.cpp", + "esphome/components/rp2040/helpers.cpp", + "esphome/components/libretiny/helpers.cpp", + "esphome/components/host/helpers.cpp", + "esphome/components/zephyr/helpers.cpp", "esphome/components/http_request/httplib.h", ], ) diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 7aa7dfe698..b1e0eaa200 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -1,29 +1,71 @@ """Fixtures for component tests.""" +from __future__ import annotations + +from collections.abc import Callable, Generator from pathlib import Path import sys +import pytest + # Add package root to python path here = Path(__file__).parent package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) -import pytest # noqa: E402 - from esphome.__main__ import generate_cpp_contents # noqa: E402 from esphome.config import read_config # noqa: E402 from esphome.core import CORE # noqa: E402 +@pytest.fixture(autouse=True) +def config_path(request: pytest.FixtureRequest) -> Generator[None]: + """Set CORE.config_path to the component's config directory and reset it after the test.""" + original_path = CORE.config_path + config_dir = Path(request.fspath).parent / "config" + + # Check if config directory exists, if not use parent directory + if config_dir.exists(): + # Set config_path to a dummy yaml file in the config directory + # This ensures CORE.config_dir points to the config directory + CORE.config_path = str(config_dir / "dummy.yaml") + else: + CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml") + + yield + CORE.config_path = original_path + + @pytest.fixture -def generate_main(): +def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: + """Return a function to get absolute paths relative to the component's fixtures directory.""" + + def _get_path(file_name: str) -> Path: + """Get the absolute path of a file relative to the component's fixtures directory.""" + return (Path(request.fspath).parent / "fixtures" / file_name).absolute() + + return _get_path + + +@pytest.fixture +def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: + """Return a function to get absolute paths relative to the component's config directory.""" + + def _get_path(file_name: str) -> Path: + """Get the absolute path of a file relative to the component's config directory.""" + return (Path(request.fspath).parent / "config" / file_name).absolute() + + return _get_path + + +@pytest.fixture +def generate_main() -> Generator[Callable[[str | Path], str]]: """Generates the C++ main.cpp file and returns it in string form.""" - def generator(path: str) -> str: - CORE.config_path = path + def generator(path: str | Path) -> str: + CORE.config_path = str(path) CORE.config = read_config({}) generate_cpp_contents(CORE.config) - print(CORE.cpp_main_section) return CORE.cpp_main_section yield generator diff --git a/tests/component_tests/image/config/bad.png b/tests/component_tests/image/config/bad.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/image/config/image.png b/tests/component_tests/image/config/image.png new file mode 100644 index 0000000000..bd2fd54783 Binary files /dev/null and b/tests/component_tests/image/config/image.png differ diff --git a/tests/component_tests/image/config/image_test.yaml b/tests/component_tests/image/config/image_test.yaml new file mode 100644 index 0000000000..3ff1260bd0 --- /dev/null +++ b/tests/component_tests/image/config/image_test.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp32: + board: esp32s3box + +image: + - file: image.png + byte_order: little_endian + id: cat_img + type: rgb565 + +spi: + mosi_pin: 6 + clk_pin: 7 + +display: + - platform: mipi_spi + id: lcd_display + model: s3box diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py new file mode 100644 index 0000000000..d8a883d32f --- /dev/null +++ b/tests/component_tests/image/test_init.py @@ -0,0 +1,183 @@ +"""Tests for image configuration validation.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.image import CONFIG_SCHEMA + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + "a string", + "Badly formed image configuration, expected a list or a dictionary", + id="invalid_string_config", + ), + pytest.param( + {"id": "image_id", "type": "rgb565"}, + r"required key not provided @ data\[0\]\['file'\]", + id="missing_file", + ), + pytest.param( + {"file": "image.png", "type": "rgb565"}, + r"required key not provided @ data\[0\]\['id'\]", + id="missing_id", + ), + pytest.param( + {"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"}, + "Could not parse mdi icon name", + id="invalid_mdi_icon", + ), + pytest.param( + { + "id": "image_id", + "file": "image.png", + "type": "binary", + "transparency": "alpha_channel", + }, + "Image format 'BINARY' cannot have transparency", + id="binary_with_transparency", + ), + pytest.param( + { + "id": "image_id", + "file": "image.png", + "type": "rgb565", + "transparency": "chroma_key", + "invert_alpha": True, + }, + "No alpha channel to invert", + id="invert_alpha_without_alpha_channel", + ), + pytest.param( + { + "id": "image_id", + "file": "image.png", + "type": "binary", + "byte_order": "big_endian", + }, + "Image format 'BINARY' does not support byte order configuration", + id="binary_with_byte_order", + ), + pytest.param( + {"id": "image_id", "file": "bad.png", "type": "binary"}, + "File can't be opened as image", + id="invalid_image_file", + ), + pytest.param( + {"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]}, + "Type is required either in the image config or in the defaults", + id="missing_type_in_defaults", + ), + ], +) +def test_image_configuration_errors( + config: Any, + error_match: str, +) -> None: + """Test detection of invalid configuration.""" + with pytest.raises(cv.Invalid, match=error_match): + CONFIG_SCHEMA(config) + + +@pytest.mark.parametrize( + "config", + [ + pytest.param( + { + "id": "image_id", + "file": "image.png", + "type": "rgb565", + "transparency": "chroma_key", + "byte_order": "little_endian", + "dither": "FloydSteinberg", + "resize": "100x100", + "invert_alpha": False, + }, + id="single_image_all_options", + ), + pytest.param( + [ + { + "id": "image_id", + "file": "image.png", + "type": "binary", + } + ], + id="list_of_images", + ), + pytest.param( + { + "defaults": { + "type": "rgb565", + "transparency": "chroma_key", + "byte_order": "little_endian", + "dither": "FloydSteinberg", + "resize": "100x100", + "invert_alpha": False, + }, + "images": [ + { + "id": "image_id", + "file": "image.png", + } + ], + }, + id="images_with_defaults", + ), + pytest.param( + { + "rgb565": { + "alpha_channel": [ + { + "id": "image_id", + "file": "image.png", + "transparency": "alpha_channel", + "byte_order": "little_endian", + "dither": "FloydSteinberg", + "resize": "100x100", + "invert_alpha": False, + } + ] + }, + "binary": [ + { + "id": "image_id", + "file": "image.png", + "transparency": "opaque", + "dither": "FloydSteinberg", + "resize": "100x100", + "invert_alpha": False, + } + ], + }, + id="type_based_organization", + ), + ], +) +def test_image_configuration_success( + config: dict[str, Any] | list[dict[str, Any]], +) -> None: + """Test successful configuration validation.""" + CONFIG_SCHEMA(config) + + +def test_image_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test image generation configuration.""" + + main_cpp = generate_main(component_config_path("image_test.yaml")) + assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp + assert ( + "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" + in main_cpp + ) diff --git a/tests/components/gl_r01_i2c/common.yaml b/tests/components/gl_r01_i2c/common.yaml new file mode 100644 index 0000000000..fe0705bdc6 --- /dev/null +++ b/tests/components/gl_r01_i2c/common.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_gl_r01_i2c + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: gl_r01_i2c + id: tof + name: "ToF sensor" + i2c_id: i2c_gl_r01_i2c + address: 0x74 + update_interval: 15s diff --git a/tests/components/gl_r01_i2c/test.esp32-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp8266-ard.yaml b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.rp2040-ard.yaml b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/image/test.esp32-ard.yaml b/tests/components/image/test.esp32-ard.yaml deleted file mode 100644 index 818e720221..0000000000 --- a/tests/components/image/test.esp32-ard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 32 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 14 - dc_pin: 13 - reset_pin: 21 - invert_colors: true - -<<: !include common.yaml - diff --git a/tests/components/image/test.esp32-c3-ard.yaml b/tests/components/image/test.esp32-c3-ard.yaml deleted file mode 100644 index 4dae9cd5ec..0000000000 --- a/tests/components/image/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,16 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 6 - mosi_pin: 7 - miso_pin: 5 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 3 - dc_pin: 11 - reset_pin: 10 - invert_colors: true - -<<: !include common.yaml diff --git a/tests/components/image/test.esp32-c3-idf.yaml b/tests/components/image/test.esp32-c3-idf.yaml deleted file mode 100644 index 4dae9cd5ec..0000000000 --- a/tests/components/image/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,16 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 6 - mosi_pin: 7 - miso_pin: 5 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 3 - dc_pin: 11 - reset_pin: 10 - invert_colors: true - -<<: !include common.yaml diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml index f963022ff4..626076d44e 100644 --- a/tests/components/image/test.esp8266-ard.yaml +++ b/tests/components/image/test.esp8266-ard.yaml @@ -13,4 +13,13 @@ display: reset_pin: 16 invert_colors: true -<<: !include common.yaml +image: + defaults: + type: rgb565 + transparency: opaque + byte_order: little_endian + resize: 50x50 + dither: FloydSteinberg + images: + - id: test_image + file: ../../pnglogo.png diff --git a/tests/components/inkplate6/common.yaml b/tests/components/inkplate6/common.yaml index 31b14e6c73..6cb5d055b6 100644 --- a/tests/components/inkplate6/common.yaml +++ b/tests/components/inkplate6/common.yaml @@ -3,6 +3,9 @@ i2c: scl: 16 sda: 17 +esp32: + cpu_frequency: 240MHz + display: - platform: inkplate6 id: inkplate_display diff --git a/tests/components/lps22/common.yaml b/tests/components/lps22/common.yaml new file mode 100644 index 0000000000..e6de4752ba --- /dev/null +++ b/tests/components/lps22/common.yaml @@ -0,0 +1,8 @@ +sensor: + - platform: lps22 + address: 0x5d + update_interval: 10s + temperature: + name: "LPS22 Temperature" + pressure: + name: "LPS22 Pressure" diff --git a/tests/components/lps22/test.esp32-ard.yaml b/tests/components/lps22/test.esp32-ard.yaml new file mode 100644 index 0000000000..0da6a9577e --- /dev/null +++ b/tests/components/lps22/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-ard.yaml b/tests/components/lps22/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-idf.yaml b/tests/components/lps22/test.esp32-idf.yaml new file mode 100644 index 0000000000..0da6a9577e --- /dev/null +++ b/tests/components/lps22/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp8266-ard.yaml b/tests/components/lps22/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.rp2040-ard.yaml b/tests/components/lps22/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml new file mode 100644 index 0000000000..3f888c3ce4 --- /dev/null +++ b/tests/components/sx126x/common.yaml @@ -0,0 +1,40 @@ +spi: + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sx126x: + dio1_pin: ${dio1_pin} + cs_pin: ${cs_pin} + busy_pin: ${busy_pin} + rst_pin: ${rst_pin} + pa_power: 3 + bandwidth: 125_0kHz + crc_enable: true + frequency: 433920000 + modulation: LORA + rx_start: true + hw_version: sx1262 + rf_switch: true + sync_value: [0x14, 0x24] + preamble_size: 8 + spreading_factor: 7 + coding_rate: CR_4_6 + tcxo_voltage: 1_8V + tcxo_delay: 5ms + on_packet: + then: + - lambda: |- + ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); + +button: + - platform: template + name: "SX126x Button" + on_press: + then: + - sx126x.set_mode_standby + - sx126x.run_image_cal + - sx126x.set_mode_sleep + - sx126x.set_mode_rx + - sx126x.send_packet: + data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] diff --git a/tests/components/sx126x/test.esp32-ard.yaml b/tests/components/sx126x/test.esp32-ard.yaml new file mode 100644 index 0000000000..9770f52229 --- /dev/null +++ b/tests/components/sx126x/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO27 + miso_pin: GPIO19 + cs_pin: GPIO18 + rst_pin: GPIO23 + busy_pin: GPIO25 + dio1_pin: GPIO26 + +<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-c3-ard.yaml b/tests/components/sx126x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..91450e24ce --- /dev/null +++ b/tests/components/sx126x/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO18 + miso_pin: GPIO19 + cs_pin: GPIO1 + rst_pin: GPIO2 + busy_pin: GPIO4 + dio1_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-c3-idf.yaml b/tests/components/sx126x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..91450e24ce --- /dev/null +++ b/tests/components/sx126x/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO18 + miso_pin: GPIO19 + cs_pin: GPIO1 + rst_pin: GPIO2 + busy_pin: GPIO4 + dio1_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-idf.yaml b/tests/components/sx126x/test.esp32-idf.yaml new file mode 100644 index 0000000000..9770f52229 --- /dev/null +++ b/tests/components/sx126x/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO27 + miso_pin: GPIO19 + cs_pin: GPIO18 + rst_pin: GPIO23 + busy_pin: GPIO25 + dio1_pin: GPIO26 + +<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp8266-ard.yaml b/tests/components/sx126x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d2c07c5bb7 --- /dev/null +++ b/tests/components/sx126x/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO1 + rst_pin: GPIO2 + busy_pin: GPIO4 + dio1_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/sx126x/test.rp2040-ard.yaml b/tests/components/sx126x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8881e96971 --- /dev/null +++ b/tests/components/sx126x/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + rst_pin: GPIO6 + busy_pin: GPIO8 + dio1_pin: GPIO7 + +<<: !include common.yaml diff --git a/tests/integration/README.md b/tests/integration/README.md index 26bd5a00ee..8fce81bb80 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -78,3 +78,268 @@ pytest -s tests/integration/test_host_mode_basic.py - Each test gets its own temporary directory and unique port - Port allocation minimizes race conditions by holding the socket until just before ESPHome starts - Output from ESPHome processes is displayed for debugging + +## Integration Test Writing Guide + +### Test Patterns and Best Practices + +#### 1. Test File Naming Convention +- Use descriptive names: `test_{category}_{feature}.py` +- Common categories: `host_mode`, `api`, `scheduler`, `light`, `areas_and_devices` +- Examples: + - `test_host_mode_basic.py` - Basic host mode functionality + - `test_api_message_batching.py` - API message batching + - `test_scheduler_stress.py` - Scheduler stress testing + +#### 2. Essential Imports +```python +from __future__ import annotations + +import asyncio +from typing import Any + +import pytest +from aioesphomeapi import EntityState, SensorState + +from .types import APIClientConnectedFactory, RunCompiledFunction +``` + +#### 3. Common Test Patterns + +##### Basic Entity Test +```python +@pytest.mark.asyncio +async def test_my_sensor( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test sensor functionality.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entity list + entities, services = await client.list_entities_services() + + # Find specific entity + sensor = next((e for e in entities if e.object_id == "my_sensor"), None) + assert sensor is not None +``` + +##### State Subscription Pattern +```python +# Track state changes with futures +loop = asyncio.get_running_loop() +states: dict[int, EntityState] = {} +state_future: asyncio.Future[EntityState] = loop.create_future() + +def on_state(state: EntityState) -> None: + states[state.key] = state + # Check for specific condition using isinstance + if isinstance(state, SensorState) and state.state == expected_value: + if not state_future.done(): + state_future.set_result(state) + +client.subscribe_states(on_state) + +# Wait for state with timeout +try: + result = await asyncio.wait_for(state_future, timeout=5.0) +except asyncio.TimeoutError: + pytest.fail(f"Expected state not received. Got: {list(states.values())}") +``` + +##### Service Execution Pattern +```python +# Find and execute service +entities, services = await client.list_entities_services() +my_service = next((s for s in services if s.name == "my_service"), None) +assert my_service is not None + +# Execute with parameters +client.execute_service(my_service, {"param1": "value1", "param2": 42}) +``` + +##### Multiple Entity Tracking +```python +# For tests with many entities +loop = asyncio.get_running_loop() +entity_count = 50 +received_states: set[int] = set() +all_states_future: asyncio.Future[bool] = loop.create_future() + +def on_state(state: EntityState) -> None: + received_states.add(state.key) + if len(received_states) >= entity_count and not all_states_future.done(): + all_states_future.set_result(True) + +client.subscribe_states(on_state) +await asyncio.wait_for(all_states_future, timeout=10.0) +``` + +#### 4. YAML Fixture Guidelines + +##### Naming Convention +- Match test function name: `test_my_feature` → `fixtures/my_feature.yaml` +- Note: Remove `test_` prefix for fixture filename + +##### Basic Structure +```yaml +esphome: + name: test-name # Use kebab-case + # Optional: areas, devices, platformio_options + +host: # Always use host platform for integration tests +api: # Port injected automatically +logger: + level: DEBUG # Optional: Set log level + +# Component configurations +sensor: + - platform: template + name: "My Sensor" + id: my_sensor + lambda: return 42.0; + update_interval: 0.1s # Fast updates for testing +``` + +##### Advanced Features +```yaml +# External components for custom test code +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH # Replaced by test framework + components: [my_test_component] + +# Areas and devices +esphome: + name: test-device + areas: + - id: living_room + name: "Living Room" + - id: kitchen + name: "Kitchen" + parent_id: living_room + devices: + - id: my_device + name: "Test Device" + area_id: living_room + +# API services +api: + services: + - service: test_service + variables: + my_param: string + then: + - logger.log: + format: "Service called with: %s" + args: [my_param.c_str()] +``` + +#### 5. Testing Complex Scenarios + +##### External Components +Create C++ components in `fixtures/external_components/` for: +- Stress testing +- Custom entity behaviors +- Scheduler testing +- Memory management tests + +##### Log Line Monitoring +```python +log_lines: list[str] = [] + +def on_log_line(line: str) -> None: + log_lines.append(line) + if "expected message" in line: + # Handle specific log messages + +async with run_compiled(yaml_config, line_callback=on_log_line): + # Test implementation +``` + +Example using futures for specific log patterns: +```python +import re + +loop = asyncio.get_running_loop() +connected_future = loop.create_future() +service_future = loop.create_future() + +# Patterns to match +connected_pattern = re.compile(r"Client .* connected from") +service_pattern = re.compile(r"Service called") + +def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not connected_future.done() and connected_pattern.search(line): + connected_future.set_result(True) + elif not service_future.done() and service_pattern.search(line): + service_future.set_result(True) + +async with run_compiled(yaml_config, line_callback=check_output): + async with api_client_connected() as client: + # Wait for specific log message + await asyncio.wait_for(connected_future, timeout=5.0) + + # Do test actions... + + # Wait for service log + await asyncio.wait_for(service_future, timeout=5.0) +``` + +**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly. + +##### Timeout Handling +```python +# Always use timeouts for async operations +try: + result = await asyncio.wait_for(some_future, timeout=5.0) +except asyncio.TimeoutError: + pytest.fail("Operation timed out - check test expectations") +``` + +#### 6. Common Assertions + +```python +# Device info +assert device_info.name == "expected-name" +assert device_info.compilation_time is not None + +# Entity properties +assert sensor.accuracy_decimals == 2 +assert sensor.state_class == 1 # measurement +assert sensor.force_update is True + +# Service availability +assert len(services) > 0 +assert any(s.name == "expected_service" for s in services) + +# State values +assert state.state == expected_value +assert state.missing_state is False +``` + +#### 7. Debugging Tips + +- Use `pytest -s` to see ESPHome output during tests +- Add descriptive failure messages to assertions +- Use `pytest.fail()` with detailed error info for timeouts +- Check `log_lines` for compilation or runtime errors +- Enable debug logging in YAML fixtures when needed + +#### 8. Performance Considerations + +- Use short update intervals (0.1s) for faster tests +- Set reasonable timeouts (5-10s for most operations) +- Batch multiple assertions when possible +- Clean up resources properly using context managers + +#### 9. Test Categories + +- **Basic Tests**: Minimal functionality verification +- **Entity Tests**: Sensor, switch, light behavior +- **API Tests**: Message batching, services, events +- **Scheduler Tests**: Timing, defer operations, stress +- **Memory Tests**: Conditional compilation, optimization +- **Integration Tests**: Areas, devices, complex interactions diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8f5f77ca52..aead6a73af 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -165,6 +165,19 @@ async def compile_esphome( """Compile an ESPHome configuration and return the binary path.""" async def _compile(config_path: Path) -> Path: + # Create a unique PlatformIO directory for this test to avoid race conditions + platformio_dir = integration_test_dir / ".platformio" + platformio_dir.mkdir(parents=True, exist_ok=True) + + # Create cache directory as well + platformio_cache_dir = platformio_dir / ".cache" + platformio_cache_dir.mkdir(parents=True, exist_ok=True) + + # Set up environment with isolated PlatformIO directories + env = os.environ.copy() + env["PLATFORMIO_CORE_DIR"] = str(platformio_dir) + env["PLATFORMIO_CACHE_DIR"] = str(platformio_cache_dir) + # Retry compilation up to 3 times if we get a segfault max_retries = 3 for attempt in range(max_retries): @@ -179,6 +192,7 @@ async def compile_esphome( stdin=asyncio.subprocess.DEVNULL, # Start in a new process group to isolate signal handling start_new_session=True, + env=env, ) await proc.wait() diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml index 4bbba5084b..22e8ed79d6 100644 --- a/tests/integration/fixtures/api_conditional_memory.yaml +++ b/tests/integration/fixtures/api_conditional_memory.yaml @@ -6,9 +6,6 @@ api: - action: test_simple_service then: - logger.log: "Simple service called" - - binary_sensor.template.publish: - id: service_called_sensor - state: ON - action: test_service_with_args variables: arg_string: string @@ -19,53 +16,14 @@ api: - logger.log: format: "Service called with: %s, %d, %d, %.2f" args: [arg_string.c_str(), arg_int, arg_bool, arg_float] - - sensor.template.publish: - id: service_arg_sensor - state: !lambda 'return arg_float;' on_client_connected: - logger.log: format: "Client %s connected from %s" args: [client_info.c_str(), client_address.c_str()] - - binary_sensor.template.publish: - id: client_connected - state: ON - - text_sensor.template.publish: - id: last_client_info - state: !lambda 'return client_info;' on_client_disconnected: - logger.log: format: "Client %s disconnected from %s" args: [client_info.c_str(), client_address.c_str()] - - binary_sensor.template.publish: - id: client_connected - state: OFF - - binary_sensor.template.publish: - id: client_disconnected_event - state: ON logger: level: DEBUG - -binary_sensor: - - platform: template - name: "Client Connected" - id: client_connected - device_class: connectivity - - platform: template - name: "Client Disconnected Event" - id: client_disconnected_event - - platform: template - name: "Service Called" - id: service_called_sensor - -sensor: - - platform: template - name: "Service Argument Value" - id: service_arg_sensor - unit_of_measurement: "" - accuracy_decimals: 2 - -text_sensor: - - platform: template - name: "Last Client Info" - id: last_client_info diff --git a/tests/integration/fixtures/entity_icon.yaml b/tests/integration/fixtures/entity_icon.yaml new file mode 100644 index 0000000000..2ce633fe2c --- /dev/null +++ b/tests/integration/fixtures/entity_icon.yaml @@ -0,0 +1,78 @@ +esphome: + name: icon-test + +host: + +api: + +logger: + +# Test entities with custom icons +sensor: + - platform: template + name: "Sensor With Icon" + icon: "mdi:temperature-celsius" + unit_of_measurement: "°C" + update_interval: 1s + lambda: |- + return 25.5; + + - platform: template + name: "Sensor Without Icon" + unit_of_measurement: "%" + update_interval: 1s + lambda: |- + return 50.0; + +binary_sensor: + - platform: template + name: "Binary Sensor With Icon" + icon: "mdi:motion-sensor" + lambda: |- + return true; + + - platform: template + name: "Binary Sensor Without Icon" + lambda: |- + return false; + +text_sensor: + - platform: template + name: "Text Sensor With Icon" + icon: "mdi:text-box" + lambda: |- + return {"Hello Icons"}; + +switch: + - platform: template + name: "Switch With Icon" + icon: "mdi:toggle-switch" + optimistic: true + +button: + - platform: template + name: "Button With Icon" + icon: "mdi:gesture-tap-button" + on_press: + - logger.log: "Button with icon pressed" + +number: + - platform: template + name: "Number With Icon" + icon: "mdi:numeric" + initial_value: 42 + min_value: 0 + max_value: 100 + step: 1 + optimistic: true + +select: + - platform: template + name: "Select With Icon" + icon: "mdi:format-list-bulleted" + options: + - "Option A" + - "Option B" + - "Option C" + initial_option: "Option A" + optimistic: true diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py new file mode 100644 index 0000000000..f32ca5f4b7 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace( + "scheduler_bulk_cleanup_component" +) +SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_( + "SchedulerBulkCleanupComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp new file mode 100644 index 0000000000..be85228c3c --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp @@ -0,0 +1,72 @@ +#include "scheduler_bulk_cleanup_component.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace scheduler_bulk_cleanup_component { + +static const char *const TAG = "bulk_cleanup"; + +void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); } + +void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { + ESP_LOGI(TAG, "Starting bulk cleanup test..."); + + // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10) + ESP_LOGI(TAG, "Scheduling 25 timeouts..."); + for (int i = 0; i < 25; i++) { + std::string name = "bulk_timeout_" + std::to_string(i); + App.scheduler.set_timeout(this, name, 2500, [i]() { + // These should never execute as we'll cancel them + ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i); + }); + } + + // Cancel all of them to mark for removal + ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup..."); + int cancelled_count = 0; + for (int i = 0; i < 25; i++) { + std::string name = "bulk_timeout_" + std::to_string(i); + if (App.scheduler.cancel_timeout(this, name)) { + cancelled_count++; + } + } + ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count); + + // At this point we have 25 items marked for removal + // The next scheduler.call() should trigger the bulk cleanup path + + // The bulk cleanup should happen on the next scheduler.call() after cancelling items + // Log that we expect bulk cleanup to be triggered + ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25); + ESP_LOGI(TAG, "Items before cleanup: 25+, after: "); + + // Schedule an interval that will execute multiple times to verify scheduler still works + static int cleanup_check_count = 0; + App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() { + cleanup_check_count++; + ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count); + + if (cleanup_check_count >= 5) { + // Cancel the interval + App.scheduler.cancel_interval(this, "cleanup_checker"); + ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup"); + } + }); + + // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup + static int post_cleanup_count = 0; + for (int i = 0; i < 5; i++) { + std::string name = "post_cleanup_" + std::to_string(i); + App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() { + ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); + post_cleanup_count++; + if (post_cleanup_count >= 5) { + ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished"); + } + }); + } +} + +} // namespace scheduler_bulk_cleanup_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h new file mode 100644 index 0000000000..f55472d426 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace scheduler_bulk_cleanup_component { + +class SchedulerBulkCleanupComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void trigger_bulk_cleanup(); +}; + +} // namespace scheduler_bulk_cleanup_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py new file mode 100644 index 0000000000..4540fa5667 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_heap_stress_component_ns = cg.esphome_ns.namespace( + "scheduler_heap_stress_component" +) +SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_( + "SchedulerHeapStressComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp new file mode 100644 index 0000000000..305d359591 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp @@ -0,0 +1,104 @@ +#include "heap_scheduler_stress_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_heap_stress_component { + +static const char *const TAG = "scheduler_heap_stress"; + +void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); } + +void SchedulerHeapStressComponent::run_multi_thread_test() { + // Use member variables instead of static to avoid issues + this->total_callbacks_ = 0; + this->executed_callbacks_ = 0; + static constexpr int NUM_THREADS = 10; + static constexpr int CALLBACKS_PER_THREAD = 100; + + ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval"); + + // Ensure we're starting clean + ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(), + this->executed_callbacks_.load()); + + // Track start time + auto start_time = std::chrono::steady_clock::now(); + + // Create threads + std::vector threads; + + ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD); + + threads.reserve(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; i++) { + threads.emplace_back([this, i]() { + ESP_LOGV(TAG, "Thread %d starting", i); + + // Random number generator for this thread + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> timeout_dist(1, 100); // 1-100ms timeouts + std::uniform_int_distribution<> interval_dist(10, 200); // 10-200ms intervals + std::uniform_int_distribution<> type_dist(0, 1); // 0=timeout, 1=interval + + // Each thread directly calls set_timeout/set_interval without any locking + for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { + int callback_id = this->total_callbacks_.fetch_add(1); + bool use_interval = (type_dist(gen) == 1); + + ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id); + + // Capture this pointer safely for the lambda + auto *component = this; + + if (use_interval) { + // Use set_interval with random interval time + uint32_t interval_ms = interval_dist(gen); + + this->set_interval(interval_ms, [component, i, j, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j); + + // Cancel the interval after first execution to avoid flooding + return false; + }); + + ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms); + } else { + // Use set_timeout with random timeout + uint32_t timeout_ms = timeout_dist(gen); + + this->set_timeout(timeout_ms, [component, i, j, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j); + }); + + ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms); + } + + // Small random delay to increase contention + if (j % 10 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + ESP_LOGV(TAG, "Thread %d finished", i); + }); + } + + // Wait for all threads to complete + for (auto &t : threads) { + t.join(); + } + + auto end_time = std::chrono::steady_clock::now(); + auto thread_time = std::chrono::duration_cast(end_time - start_time).count(); + ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load()); +} + +} // namespace scheduler_heap_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h new file mode 100644 index 0000000000..5da32ca9f8 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_heap_stress_component { + +class SchedulerHeapStressComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_multi_thread_test(); + + private: + std::atomic total_callbacks_{0}; + std::atomic executed_callbacks_{0}; +}; + +} // namespace scheduler_heap_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py new file mode 100644 index 0000000000..0bb784e74e --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace( + "scheduler_rapid_cancellation_component" +) +SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_( + "SchedulerRapidCancellationComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp new file mode 100644 index 0000000000..b735c453f2 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp @@ -0,0 +1,80 @@ +#include "rapid_cancellation_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_rapid_cancellation_component { + +static const char *const TAG = "scheduler_rapid_cancellation"; + +void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); } + +void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { + ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names"); + + // Reset counters + this->total_scheduled_ = 0; + this->total_executed_ = 0; + + static constexpr int NUM_THREADS = 4; // Number of threads to create + static constexpr int NUM_NAMES = 10; // Only 10 unique names + static constexpr int OPERATIONS_PER_THREAD = 100; // Each thread does 100 operations + + // Create threads that will all fight over the same timeout names + std::vector threads; + threads.reserve(NUM_THREADS); + + for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { + threads.emplace_back([this]() { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + // Use modulo to ensure multiple threads use the same names + int name_index = i % NUM_NAMES; + std::stringstream ss; + ss << "shared_timeout_" << name_index; + std::string name = ss.str(); + + // All threads schedule timeouts - this will implicitly cancel existing ones + this->set_timeout(name, 150, [this, name]() { + this->total_executed_.fetch_add(1); + ESP_LOGI(TAG, "Executed callback '%s'", name.c_str()); + }); + this->total_scheduled_.fetch_add(1); + + // Small delay to increase chance of race conditions + if (i % 10 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + }); + } + + // Wait for all threads to complete + for (auto &t : threads) { + t.join(); + } + + ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load()); + + // Give some time for any remaining callbacks to execute + this->set_timeout("final_timeout", 200, [this]() { + ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:"); + ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load()); + ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load()); + + // Calculate implicit cancellations (timeouts replaced when scheduling same name) + int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load(); + ESP_LOGI(TAG, " Implicit cancellations (replaced): %d", implicit_cancellations); + ESP_LOGI(TAG, " Total accounted: %d (executed + implicit cancellations)", + this->total_executed_.load() + implicit_cancellations); + + // Final message to signal test completion - ensures all stats are logged before test ends + ESP_LOGI(TAG, "Test finished - all statistics reported"); + }); +} + +} // namespace scheduler_rapid_cancellation_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h new file mode 100644 index 0000000000..0a01b2a8de --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_rapid_cancellation_component { + +class SchedulerRapidCancellationComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_rapid_cancellation_test(); + + private: + std::atomic total_scheduled_{0}; + std::atomic total_executed_{0}; +}; + +} // namespace scheduler_rapid_cancellation_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py new file mode 100644 index 0000000000..4e847a6fdb --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace( + "scheduler_recursive_timeout_component" +) +SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_( + "SchedulerRecursiveTimeoutComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp new file mode 100644 index 0000000000..2a08bd72a9 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp @@ -0,0 +1,40 @@ +#include "recursive_timeout_component.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace scheduler_recursive_timeout_component { + +static const char *const TAG = "scheduler_recursive_timeout"; + +void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); } + +void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() { + ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout"); + + // Reset state + this->nested_level_ = 0; + + // Schedule the initial timeout with 1ms delay + this->set_timeout(1, [this]() { + ESP_LOGI(TAG, "Executing initial timeout"); + this->nested_level_ = 1; + + // From within this timeout, schedule another timeout with 1ms delay + this->set_timeout(1, [this]() { + ESP_LOGI(TAG, "Executing nested timeout 1"); + this->nested_level_ = 2; + + // From within this nested timeout, schedule yet another timeout with 1ms delay + this->set_timeout(1, [this]() { + ESP_LOGI(TAG, "Executing nested timeout 2"); + this->nested_level_ = 3; + + // Test complete + ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_); + }); + }); + }); +} + +} // namespace scheduler_recursive_timeout_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h new file mode 100644 index 0000000000..8d2c085a11 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" + +namespace esphome { +namespace scheduler_recursive_timeout_component { + +class SchedulerRecursiveTimeoutComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_recursive_timeout_test(); + + private: + int nested_level_{0}; +}; + +} // namespace scheduler_recursive_timeout_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py new file mode 100644 index 0000000000..bb1d560ad3 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace( + "scheduler_simultaneous_callbacks_component" +) +SchedulerSimultaneousCallbacksComponent = ( + scheduler_simultaneous_callbacks_component_ns.class_( + "SchedulerSimultaneousCallbacksComponent", cg.Component + ) +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp new file mode 100644 index 0000000000..b4c2b8c6c2 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp @@ -0,0 +1,109 @@ +#include "simultaneous_callbacks_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_simultaneous_callbacks_component { + +static const char *const TAG = "scheduler_simultaneous_callbacks"; + +void SchedulerSimultaneousCallbacksComponent::setup() { + ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup"); +} + +void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() { + ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now"); + + // Reset counters + this->total_scheduled_ = 0; + this->total_executed_ = 0; + this->callbacks_at_once_ = 0; + this->max_concurrent_ = 0; + + static constexpr int NUM_THREADS = 10; + static constexpr int CALLBACKS_PER_THREAD = 100; + static constexpr uint32_t DELAY_MS = 1; // All callbacks scheduled for 1ms from now + + // Create threads for concurrent scheduling + std::vector threads; + threads.reserve(NUM_THREADS); + + // Record start time for synchronization + auto start_time = std::chrono::steady_clock::now(); + + for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { + threads.emplace_back([this, thread_id, start_time]() { + ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id); + + // Wait a tiny bit to ensure all threads start roughly together + std::this_thread::sleep_until(start_time + std::chrono::microseconds(100)); + + for (int i = 0; i < CALLBACKS_PER_THREAD; i++) { + // Create unique name for each callback + std::stringstream ss; + ss << "thread_" << thread_id << "_cb_" << i; + std::string name = ss.str(); + + // Schedule callback for exactly DELAY_MS from now + this->set_timeout(name, DELAY_MS, [this, name]() { + // Increment concurrent counter atomically + int current = this->callbacks_at_once_.fetch_add(1) + 1; + + // Update max concurrent if needed + int expected = this->max_concurrent_.load(); + while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) { + // Loop until we successfully update or someone else set a higher value + } + + ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current); + + // Simulate some minimal work + std::atomic work{0}; + for (int j = 0; j < 10; j++) { + work.fetch_add(j); + } + + // Increment executed counter + this->total_executed_.fetch_add(1); + + // Decrement concurrent counter + this->callbacks_at_once_.fetch_sub(1); + }); + + this->total_scheduled_.fetch_add(1); + ESP_LOGV(TAG, "Scheduled callback %s", name.c_str()); + } + + ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id); + }); + } + + // Wait for all threads to complete scheduling + for (auto &t : threads) { + t.join(); + } + + ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load()); + + // Schedule a final timeout to check results after all callbacks should have executed + this->set_timeout("final_check", 100, [this]() { + ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load()); + ESP_LOGI(TAG, "Statistics:"); + ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load()); + ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load()); + ESP_LOGI(TAG, " Max concurrent callbacks: %d", this->max_concurrent_.load()); + + if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) { + ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load()); + } else { + ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD, + this->total_executed_.load()); + } + }); +} + +} // namespace scheduler_simultaneous_callbacks_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h new file mode 100644 index 0000000000..1a36af4b3d --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_simultaneous_callbacks_component { + +class SchedulerSimultaneousCallbacksComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_simultaneous_callbacks_test(); + + private: + std::atomic total_scheduled_{0}; + std::atomic total_executed_{0}; + std::atomic callbacks_at_once_{0}; + std::atomic max_concurrent_{0}; +}; + +} // namespace scheduler_simultaneous_callbacks_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py new file mode 100644 index 0000000000..3f29a839ef --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace( + "scheduler_string_lifetime_component" +) +SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_( + "SchedulerStringLifetimeComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp new file mode 100644 index 0000000000..ea386881b2 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp @@ -0,0 +1,275 @@ +#include "string_lifetime_component.h" +#include "esphome/core/log.h" +#include +#include +#include + +namespace esphome { +namespace scheduler_string_lifetime_component { + +static const char *const TAG = "scheduler_string_lifetime"; + +void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); } + +void SchedulerStringLifetimeComponent::run_string_lifetime_test() { + ESP_LOGI(TAG, "Starting string lifetime tests"); + + this->tests_passed_ = 0; + this->tests_failed_ = 0; + + // Run each test + test_temporary_string_lifetime(); + test_scope_exit_string(); + test_vector_reallocation(); + test_string_move_semantics(); + test_lambda_capture_lifetime(); + + // Schedule final check + this->set_timeout("final_check", 200, [this]() { + ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); + ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); + + if (this->tests_failed_ == 0) { + ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); + } else { + ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); + } + ESP_LOGI(TAG, "String lifetime tests complete"); + }); +} + +void SchedulerStringLifetimeComponent::run_test1() { + test_temporary_string_lifetime(); + // Wait for all callbacks to execute + this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test2() { + test_scope_exit_string(); + // Wait for all callbacks to execute + this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test3() { + test_vector_reallocation(); + // Wait for all callbacks to execute + this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test4() { + test_string_move_semantics(); + // Wait for all callbacks to execute + this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test5() { + test_lambda_capture_lifetime(); + // Wait for all callbacks to execute + this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_final_check() { + ESP_LOGI(TAG, "String lifetime tests complete"); + ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); + ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); + + if (this->tests_failed_ == 0) { + ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); + } else { + ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); + } +} + +void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() { + ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names"); + + // Test with a temporary string that goes out of scope immediately + { + std::string temp_name = "temp_callback_" + std::to_string(12345); + + // Schedule with temporary string name - scheduler must copy/store this + this->set_timeout(temp_name, 1, [this]() { + ESP_LOGD(TAG, "Callback for temp string name executed"); + this->tests_passed_++; + }); + + // String goes out of scope here, but scheduler should have made a copy + } + + // Test with rvalue string as name + this->set_timeout(std::string("rvalue_test"), 2, [this]() { + ESP_LOGD(TAG, "Rvalue string name callback executed"); + this->tests_passed_++; + }); + + // Test cancelling with reconstructed string + { + std::string cancel_name = "cancel_test_" + std::to_string(999); + this->set_timeout(cancel_name, 100, [this]() { + ESP_LOGE(TAG, "This should have been cancelled!"); + this->tests_failed_++; + }); + } // cancel_name goes out of scope + + // Reconstruct the same string to cancel + std::string cancel_name_2 = "cancel_test_" + std::to_string(999); + bool cancelled = this->cancel_timeout(cancel_name_2); + if (cancelled) { + ESP_LOGD(TAG, "Successfully cancelled with reconstructed string"); + this->tests_passed_++; + } else { + ESP_LOGE(TAG, "Failed to cancel with reconstructed string"); + this->tests_failed_++; + } +} + +void SchedulerStringLifetimeComponent::test_scope_exit_string() { + ESP_LOGI(TAG, "Test 2: Scope exit string names"); + + // Create string names in a limited scope + { + std::string scoped_name = "scoped_timeout_" + std::to_string(555); + + // Schedule with scoped string name + this->set_timeout(scoped_name, 3, [this]() { + ESP_LOGD(TAG, "Scoped name callback executed"); + this->tests_passed_++; + }); + + // scoped_name goes out of scope here + } + + // Test with dynamically allocated string name + { + auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777)); + + this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() { + ESP_LOGD(TAG, "Dynamic string name callback executed"); + this->tests_passed_++; + delete dynamic_name; // Clean up in callback + }); + + // Pointer goes out of scope but string object remains until callback + } + + // Test multiple timeouts with same dynamically created name + for (int i = 0; i < 3; i++) { + std::string loop_name = "loop_timeout_" + std::to_string(i); + this->set_timeout(loop_name, 5 + i * 1, [this, i]() { + ESP_LOGD(TAG, "Loop timeout %d executed", i); + this->tests_passed_++; + }); + // loop_name destroyed and recreated each iteration + } +} + +void SchedulerStringLifetimeComponent::test_vector_reallocation() { + ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names"); + + // Create a vector that will reallocate + std::vector names; + names.reserve(2); // Small initial capacity to force reallocation + + // Schedule callbacks with string names from vector + for (int i = 0; i < 10; i++) { + names.push_back("vector_cb_" + std::to_string(i)); + // Use the string from vector as timeout name + this->set_timeout(names.back(), 8 + i * 1, [this, i]() { + ESP_LOGV(TAG, "Vector name callback %d executed", i); + this->tests_passed_++; + }); + } + + // Force reallocation by adding more elements + // This will move all strings to new memory locations + for (int i = 10; i < 50; i++) { + names.push_back("realloc_trigger_" + std::to_string(i)); + } + + // Add more timeouts after reallocation to ensure old names still work + for (int i = 50; i < 55; i++) { + names.push_back("post_realloc_" + std::to_string(i)); + this->set_timeout(names.back(), 20 + (i - 50), [this]() { + ESP_LOGV(TAG, "Post-reallocation callback executed"); + this->tests_passed_++; + }); + } + + // Clear the vector while timeouts are still pending + names.clear(); + ESP_LOGD(TAG, "Vector cleared - all string names destroyed"); +} + +void SchedulerStringLifetimeComponent::test_string_move_semantics() { + ESP_LOGI(TAG, "Test 4: String move semantics for timeout names"); + + // Test moving string names + std::string original = "move_test_original"; + std::string moved = std::move(original); + + // Schedule with moved string as name + this->set_timeout(moved, 30, [this]() { + ESP_LOGD(TAG, "Moved string name callback executed"); + this->tests_passed_++; + }); + + // original is now empty, try to use it as a different timeout name + original = "reused_after_move"; + this->set_timeout(original, 32, [this]() { + ESP_LOGD(TAG, "Reused string name callback executed"); + this->tests_passed_++; + }); +} + +void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() { + ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios"); + + // Test scheduling with name built in lambda + [this]() { + std::string lambda_name = "lambda_built_name_" + std::to_string(888); + this->set_timeout(lambda_name, 38, [this]() { + ESP_LOGD(TAG, "Lambda-built name callback executed"); + this->tests_passed_++; + }); + }(); // Lambda executes and lambda_name is destroyed + + // Test with shared_ptr name + auto shared_name = std::make_shared("shared_ptr_timeout"); + this->set_timeout(*shared_name, 40, [this, shared_name]() { + ESP_LOGD(TAG, "Shared_ptr name callback executed"); + this->tests_passed_++; + }); + shared_name.reset(); // Release the shared_ptr + + // Test overwriting timeout with same name + std::string overwrite_name = "overwrite_test"; + this->set_timeout(overwrite_name, 1000, [this]() { + ESP_LOGE(TAG, "This should have been overwritten!"); + this->tests_failed_++; + }); + + // Overwrite with shorter timeout + this->set_timeout(overwrite_name, 42, [this]() { + ESP_LOGD(TAG, "Overwritten timeout executed"); + this->tests_passed_++; + }); + + // Test very long string name + std::string long_name; + for (int i = 0; i < 100; i++) { + long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_"; + } + this->set_timeout(long_name, 44, [this]() { + ESP_LOGD(TAG, "Very long name timeout executed"); + this->tests_passed_++; + }); + + // Test empty string as name + this->set_timeout("", 46, [this]() { + ESP_LOGD(TAG, "Empty string name timeout executed"); + this->tests_passed_++; + }); +} + +} // namespace scheduler_string_lifetime_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h new file mode 100644 index 0000000000..95532328bb --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include +#include + +namespace esphome { +namespace scheduler_string_lifetime_component { + +class SchedulerStringLifetimeComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_string_lifetime_test(); + + // Individual test methods exposed as services + void run_test1(); + void run_test2(); + void run_test3(); + void run_test4(); + void run_test5(); + void run_final_check(); + + private: + void test_temporary_string_lifetime(); + void test_scope_exit_string(); + void test_vector_reallocation(); + void test_string_move_semantics(); + void test_lambda_capture_lifetime(); + + int tests_passed_{0}; + int tests_failed_{0}; +}; + +} // namespace scheduler_string_lifetime_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py new file mode 100644 index 0000000000..6cc564395c --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace( + "scheduler_string_name_stress_component" +) +SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_( + "SchedulerStringNameStressComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp new file mode 100644 index 0000000000..9071e573bb --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp @@ -0,0 +1,110 @@ +#include "string_name_stress_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_string_name_stress_component { + +static const char *const TAG = "scheduler_string_name_stress"; + +void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); } + +void SchedulerStringNameStressComponent::run_string_name_stress_test() { + // Use member variables to reset state + this->total_callbacks_ = 0; + this->executed_callbacks_ = 0; + static constexpr int NUM_THREADS = 10; + static constexpr int CALLBACKS_PER_THREAD = 100; + + ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names"); + ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management"); + + // Track start time + auto start_time = std::chrono::steady_clock::now(); + + // Create threads + std::vector threads; + + ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS, + CALLBACKS_PER_THREAD); + + threads.reserve(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; i++) { + threads.emplace_back([this, i]() { + ESP_LOGV(TAG, "Thread %d starting", i); + + // Each thread schedules callbacks with dynamically created string names + for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { + int callback_id = this->total_callbacks_.fetch_add(1); + + // Create a dynamic string name - this will test memory management + std::stringstream ss; + ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id; + std::string dynamic_name = ss.str(); + + ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str()); + + // Capture necessary values for the lambda + auto *component = this; + + // Schedule with std::string name - this tests the string overload + // Use varying delays to stress the heap scheduler + uint32_t delay = 1 + (callback_id % 50); + + // Also test nested scheduling from callbacks + if (j % 10 == 0) { + // Every 10th callback schedules another callback + this->set_timeout(dynamic_name, delay, [component, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id); + + // Schedule another timeout from within this callback with a new dynamic name + std::string nested_name = "nested_from_" + std::to_string(callback_id); + component->set_timeout(nested_name, 1, [callback_id]() { + ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id); + }); + }); + } else { + // Regular callback + this->set_timeout(dynamic_name, delay, [component, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed string-named callback %d", callback_id); + }); + } + + // Add some timing variations to increase race conditions + if (j % 5 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + ESP_LOGV(TAG, "Thread %d finished scheduling", i); + }); + } + + // Wait for all threads to complete scheduling + for (auto &t : threads) { + t.join(); + } + + auto end_time = std::chrono::steady_clock::now(); + auto thread_time = std::chrono::duration_cast(end_time - start_time).count(); + ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time, + this->total_callbacks_.load()); + + // Give some time for callbacks to execute + ESP_LOGI(TAG, "Waiting for callbacks to execute..."); + + // Schedule a final callback to signal completion + this->set_timeout("test_complete", 2000, [this]() { + ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(), + this->total_callbacks_.load()); + }); +} + +} // namespace scheduler_string_name_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h new file mode 100644 index 0000000000..002a0a7b51 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_string_name_stress_component { + +class SchedulerStringNameStressComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_string_name_stress_test(); + + private: + std::atomic total_callbacks_{0}; + std::atomic executed_callbacks_{0}; +}; + +} // namespace scheduler_string_name_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/light_calls.yaml b/tests/integration/fixtures/light_calls.yaml new file mode 100644 index 0000000000..d692a11765 --- /dev/null +++ b/tests/integration/fixtures/light_calls.yaml @@ -0,0 +1,80 @@ +esphome: + name: light-calls-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +# Test outputs for RGBCW light +output: + - platform: template + id: test_red + type: float + write_action: + - logger.log: + format: "Red output: %.2f" + args: [state] + - platform: template + id: test_green + type: float + write_action: + - logger.log: + format: "Green output: %.2f" + args: [state] + - platform: template + id: test_blue + type: float + write_action: + - logger.log: + format: "Blue output: %.2f" + args: [state] + - platform: template + id: test_cold_white + type: float + write_action: + - logger.log: + format: "Cold white output: %.2f" + args: [state] + - platform: template + id: test_warm_white + type: float + write_action: + - logger.log: + format: "Warm white output: %.2f" + args: [state] + +light: + - platform: rgbww + name: "Test RGBCW Light" + id: test_light + red: test_red + green: test_green + blue: test_blue + cold_white: test_cold_white + warm_white: test_warm_white + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + effects: + - random: + name: "Random Effect" + transition_length: 100ms + update_interval: 200ms + - strobe: + name: "Strobe Effect" + - pulse: + name: "Pulse Effect" + transition_length: 100ms + + # Additional lights to test memory with multiple instances + - platform: rgb + name: "Test RGB Light" + id: test_rgb_light + red: test_red + green: test_green + blue: test_blue + + - platform: binary + name: "Test Binary Light" + id: test_binary_light + output: test_red diff --git a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml new file mode 100644 index 0000000000..de876da8c4 --- /dev/null +++ b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml @@ -0,0 +1,23 @@ +esphome: + name: scheduler-bulk-cleanup + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +host: + +logger: + level: DEBUG + +api: + services: + - service: trigger_bulk_cleanup + then: + - lambda: |- + auto component = id(bulk_cleanup_component); + component->trigger_bulk_cleanup(); + +scheduler_bulk_cleanup_component: + id: bulk_cleanup_component diff --git a/tests/integration/fixtures/scheduler_defer_cancel.yaml b/tests/integration/fixtures/scheduler_defer_cancel.yaml new file mode 100644 index 0000000000..9e3f927c33 --- /dev/null +++ b/tests/integration/fixtures/scheduler_defer_cancel.yaml @@ -0,0 +1,51 @@ +esphome: + name: scheduler-defer-cancel + +host: + +logger: + level: DEBUG + +api: + services: + - service: test_defer_cancel + then: + - lambda: |- + // Schedule 10 defers with the same name + // Only the last one should execute + for (int i = 1; i <= 10; i++) { + App.scheduler.set_timeout(nullptr, "test_defer", 0, [i]() { + ESP_LOGI("TEST", "Defer executed: %d", i); + // Fire event with the defer number + std::string event_type = "defer_executed_" + std::to_string(i); + id(test_result)->trigger(event_type); + }); + } + + // Schedule completion notification after all defers + App.scheduler.set_timeout(nullptr, "completion", 0, []() { + ESP_LOGI("TEST", "Test complete"); + id(test_complete)->trigger("test_finished"); + }); + +event: + - platform: template + id: test_result + name: "Test Result" + event_types: + - "defer_executed_1" + - "defer_executed_2" + - "defer_executed_3" + - "defer_executed_4" + - "defer_executed_5" + - "defer_executed_6" + - "defer_executed_7" + - "defer_executed_8" + - "defer_executed_9" + - "defer_executed_10" + + - platform: template + id: test_complete + name: "Test Complete" + event_types: + - "test_finished" diff --git a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml new file mode 100644 index 0000000000..fb6b1791dc --- /dev/null +++ b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml @@ -0,0 +1,34 @@ +esphome: + name: scheduler-defer-cancel-regular + +host: + +logger: + level: DEBUG + +api: + services: + - service: test_defer_cancels_regular + then: + - lambda: |- + ESP_LOGI("TEST", "Starting defer cancels regular timeout test"); + + // Schedule a regular timeout with 100ms delay + App.scheduler.set_timeout(nullptr, "test_timeout", 100, []() { + ESP_LOGE("TEST", "ERROR: Regular timeout executed - should have been cancelled!"); + }); + + ESP_LOGI("TEST", "Scheduled regular timeout with 100ms delay"); + + // Immediately schedule a deferred timeout (0 delay) with the same name + // This should cancel the regular timeout + App.scheduler.set_timeout(nullptr, "test_timeout", 0, []() { + ESP_LOGI("TEST", "SUCCESS: Deferred timeout executed"); + }); + + ESP_LOGI("TEST", "Scheduled deferred timeout - should cancel regular timeout"); + + // Schedule test completion after 200ms (after regular timeout would have fired) + App.scheduler.set_timeout(nullptr, "test_complete", 200, []() { + ESP_LOGI("TEST", "Test complete"); + }); diff --git a/tests/integration/fixtures/defer_fifo_simple.yaml b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml similarity index 99% rename from tests/integration/fixtures/defer_fifo_simple.yaml rename to tests/integration/fixtures/scheduler_defer_fifo_simple.yaml index db24ebf601..7384082ac2 100644 --- a/tests/integration/fixtures/defer_fifo_simple.yaml +++ b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml @@ -1,5 +1,5 @@ esphome: - name: defer-fifo-simple + name: scheduler-defer-fifo-simple host: diff --git a/tests/integration/fixtures/defer_stress.yaml b/tests/integration/fixtures/scheduler_defer_stress.yaml similarity index 94% rename from tests/integration/fixtures/defer_stress.yaml rename to tests/integration/fixtures/scheduler_defer_stress.yaml index 6df475229b..0d9c1d1405 100644 --- a/tests/integration/fixtures/defer_stress.yaml +++ b/tests/integration/fixtures/scheduler_defer_stress.yaml @@ -1,5 +1,5 @@ esphome: - name: defer-stress-test + name: scheduler-defer-stress-test external_components: - source: diff --git a/tests/integration/fixtures/scheduler_heap_stress.yaml b/tests/integration/fixtures/scheduler_heap_stress.yaml new file mode 100644 index 0000000000..d4d340b68b --- /dev/null +++ b/tests/integration/fixtures/scheduler_heap_stress.yaml @@ -0,0 +1,38 @@ +esphome: + name: scheduler-heap-stress-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_heap_stress_component] + +host: + +logger: + level: VERBOSE + +scheduler_heap_stress_component: + id: heap_stress + +api: + services: + - service: run_heap_stress_test + then: + - lambda: |- + id(heap_stress)->run_multi_thread_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml new file mode 100644 index 0000000000..4824654c5c --- /dev/null +++ b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml @@ -0,0 +1,38 @@ +esphome: + name: sched-rapid-cancel-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_rapid_cancellation_component] + +host: + +logger: + level: VERBOSE + +scheduler_rapid_cancellation_component: + id: rapid_cancel + +api: + services: + - service: run_rapid_cancellation_test + then: + - lambda: |- + id(rapid_cancel)->run_rapid_cancellation_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/fixtures/scheduler_recursive_timeout.yaml b/tests/integration/fixtures/scheduler_recursive_timeout.yaml new file mode 100644 index 0000000000..f1168802f6 --- /dev/null +++ b/tests/integration/fixtures/scheduler_recursive_timeout.yaml @@ -0,0 +1,38 @@ +esphome: + name: sched-recursive-timeout + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_recursive_timeout_component] + +host: + +logger: + level: VERBOSE + +scheduler_recursive_timeout_component: + id: recursive_timeout + +api: + services: + - service: run_recursive_timeout_test + then: + - lambda: |- + id(recursive_timeout)->run_recursive_timeout_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml new file mode 100644 index 0000000000..446ee7fdc0 --- /dev/null +++ b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml @@ -0,0 +1,23 @@ +esphome: + name: sched-simul-callbacks-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_simultaneous_callbacks_component] + +host: + +logger: + level: INFO + +scheduler_simultaneous_callbacks_component: + id: simultaneous_callbacks + +api: + services: + - service: run_simultaneous_callbacks_test + then: + - lambda: |- + id(simultaneous_callbacks)->run_simultaneous_callbacks_test(); diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml new file mode 100644 index 0000000000..ebd5052b8b --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_lifetime.yaml @@ -0,0 +1,47 @@ +esphome: + name: scheduler-string-lifetime-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_string_lifetime_component] + +host: + +logger: + level: DEBUG + +scheduler_string_lifetime_component: + id: string_lifetime + +api: + services: + - service: run_string_lifetime_test + then: + - lambda: |- + id(string_lifetime)->run_string_lifetime_test(); + - service: run_test1 + then: + - lambda: |- + id(string_lifetime)->run_test1(); + - service: run_test2 + then: + - lambda: |- + id(string_lifetime)->run_test2(); + - service: run_test3 + then: + - lambda: |- + id(string_lifetime)->run_test3(); + - service: run_test4 + then: + - lambda: |- + id(string_lifetime)->run_test4(); + - service: run_test5 + then: + - lambda: |- + id(string_lifetime)->run_test5(); + - service: run_final_check + then: + - lambda: |- + id(string_lifetime)->run_final_check(); diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml new file mode 100644 index 0000000000..d1ef55c8d5 --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_name_stress.yaml @@ -0,0 +1,38 @@ +esphome: + name: sched-string-name-stress + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_string_name_stress_component] + +host: + +logger: + level: VERBOSE + +scheduler_string_name_stress_component: + id: string_stress + +api: + services: + - service: run_string_name_stress_test + then: + - lambda: |- + id(string_stress)->run_string_name_stress_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py index 8048624f70..cfa32c431d 100644 --- a/tests/integration/test_api_conditional_memory.py +++ b/tests/integration/test_api_conditional_memory.py @@ -3,15 +3,9 @@ from __future__ import annotations import asyncio +import re -from aioesphomeapi import ( - BinarySensorInfo, - EntityState, - SensorInfo, - TextSensorInfo, - UserService, - UserServiceArgType, -) +from aioesphomeapi import UserService, UserServiceArgType import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -25,50 +19,45 @@ async def test_api_conditional_memory( ) -> None: """Test API triggers and services work correctly with conditional compilation.""" loop = asyncio.get_running_loop() - # Keep ESPHome process running throughout the test - async with run_compiled(yaml_config): - # First connection + + # Track log messages + connected_future = loop.create_future() + disconnected_future = loop.create_future() + service_simple_future = loop.create_future() + service_args_future = loop.create_future() + + # Patterns to match in logs + connected_pattern = re.compile(r"Client .* connected from") + disconnected_pattern = re.compile(r"Client .* disconnected from") + service_simple_pattern = re.compile(r"Simple service called") + service_args_pattern = re.compile( + r"Service called with: test_string, 123, 1, 42\.50" + ) + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not connected_future.done() and connected_pattern.search(line): + connected_future.set_result(True) + elif not disconnected_future.done() and disconnected_pattern.search(line): + disconnected_future.set_result(True) + elif not service_simple_future.done() and service_simple_pattern.search(line): + service_simple_future.set_result(True) + elif not service_args_future.done() and service_args_pattern.search(line): + service_args_future.set_result(True) + + # Run with log monitoring + async with run_compiled(yaml_config, line_callback=check_output): async with api_client_connected() as client: # Verify device info device_info = await client.device_info() assert device_info is not None assert device_info.name == "api-conditional-memory-test" - # List entities and services - entity_info, services = await asyncio.wait_for( - client.list_entities_services(), timeout=5.0 - ) + # Wait for connection log + await asyncio.wait_for(connected_future, timeout=5.0) - # Find our entities - client_connected: BinarySensorInfo | None = None - client_disconnected_event: BinarySensorInfo | None = None - service_called_sensor: BinarySensorInfo | None = None - service_arg_sensor: SensorInfo | None = None - last_client_info: TextSensorInfo | None = None - - for entity in entity_info: - if isinstance(entity, BinarySensorInfo): - if entity.object_id == "client_connected": - client_connected = entity - elif entity.object_id == "client_disconnected_event": - client_disconnected_event = entity - elif entity.object_id == "service_called": - service_called_sensor = entity - elif isinstance(entity, SensorInfo): - if entity.object_id == "service_argument_value": - service_arg_sensor = entity - elif isinstance(entity, TextSensorInfo): - if entity.object_id == "last_client_info": - last_client_info = entity - - # Verify all entities exist - assert client_connected is not None, "client_connected sensor not found" - assert client_disconnected_event is not None, ( - "client_disconnected_event sensor not found" - ) - assert service_called_sensor is not None, "service_called sensor not found" - assert service_arg_sensor is not None, "service_arg_sensor not found" - assert last_client_info is not None, "last_client_info sensor not found" + # List services + _, services = await client.list_entities_services() # Verify services exist assert len(services) == 2, f"Expected 2 services, found {len(services)}" @@ -98,66 +87,11 @@ async def test_api_conditional_memory( assert arg_types["arg_bool"] == UserServiceArgType.BOOL assert arg_types["arg_float"] == UserServiceArgType.FLOAT - # Track state changes - states: dict[int, EntityState] = {} - states_future: asyncio.Future[None] = loop.create_future() - - def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have initial states for connection sensors - if ( - client_connected.key in states - and last_client_info.key in states - and not states_future.done() - ): - states_future.set_result(None) - - client.subscribe_states(on_state) - - # Wait for initial states - await asyncio.wait_for(states_future, timeout=5.0) - - # Verify on_client_connected trigger fired - connected_state = states.get(client_connected.key) - assert connected_state is not None - assert connected_state.state is True, "Client should be connected" - - # Verify client info was captured - client_info_state = states.get(last_client_info.key) - assert client_info_state is not None - assert isinstance(client_info_state.state, str) - assert len(client_info_state.state) > 0, "Client info should not be empty" - - # Test simple service - service_future: asyncio.Future[None] = loop.create_future() - - def check_service_called(state: EntityState) -> None: - if state.key == service_called_sensor.key and state.state is True: - if not service_future.done(): - service_future.set_result(None) - - # Update callback to check for service execution - client.subscribe_states(check_service_called) - # Call simple service client.execute_service(simple_service, {}) - # Wait for service to execute - await asyncio.wait_for(service_future, timeout=5.0) - - # Test service with arguments - arg_future: asyncio.Future[None] = loop.create_future() - expected_float = 42.5 - - def check_arg_sensor(state: EntityState) -> None: - if ( - state.key == service_arg_sensor.key - and abs(state.state - expected_float) < 0.01 - ): - if not arg_future.done(): - arg_future.set_result(None) - - client.subscribe_states(check_arg_sensor) + # Wait for service log + await asyncio.wait_for(service_simple_future, timeout=5.0) # Call service with arguments client.execute_service( @@ -166,43 +100,12 @@ async def test_api_conditional_memory( "arg_string": "test_string", "arg_int": 123, "arg_bool": True, - "arg_float": expected_float, + "arg_float": 42.5, }, ) - # Wait for service with args to execute - await asyncio.wait_for(arg_future, timeout=5.0) + # Wait for service with args log + await asyncio.wait_for(service_args_future, timeout=5.0) - # After disconnecting first client, reconnect and verify triggers work - async with api_client_connected() as client2: - # Subscribe to states with new client - states2: dict[int, EntityState] = {} - states_ready_future: asyncio.Future[None] = loop.create_future() - - def on_state2(state: EntityState) -> None: - states2[state.key] = state - # Check if we have received both required states - if ( - client_connected.key in states2 - and client_disconnected_event.key in states2 - and not states_ready_future.done() - ): - states_ready_future.set_result(None) - - client2.subscribe_states(on_state2) - - # Wait for both connected and disconnected event states - await asyncio.wait_for(states_ready_future, timeout=5.0) - - # Verify client is connected again (on_client_connected fired) - assert states2[client_connected.key].state is True, ( - "Client should be reconnected" - ) - - # The client_disconnected_event should be ON from when we disconnected - # (it was set ON by on_client_disconnected trigger) - disconnected_state = states2.get(client_disconnected_event.key) - assert disconnected_state is not None - assert disconnected_state.state is True, ( - "Disconnect event should be ON from previous disconnect" - ) + # Client disconnected here, wait for disconnect log + await asyncio.wait_for(disconnected_future, timeout=5.0) diff --git a/tests/integration/test_api_vv_logging.py b/tests/integration/test_api_vv_logging.py index 19aab2001c..fcbdd341ae 100644 --- a/tests/integration/test_api_vv_logging.py +++ b/tests/integration/test_api_vv_logging.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio from typing import Any -from aioesphomeapi import LogLevel +from aioesphomeapi import LogLevel, SensorInfo import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -63,7 +63,7 @@ async def test_api_vv_logging( entity_info, _ = await client.list_entities_services() # Count sensors - sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement")) + sensor_count = sum(1 for e in entity_info if isinstance(e, SensorInfo)) assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}" # Wait for sensor updates to flow with VV logging active diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py index 4ce55a30a7..4184255724 100644 --- a/tests/integration/test_areas_and_devices.py +++ b/tests/integration/test_areas_and_devices.py @@ -76,8 +76,8 @@ async def test_areas_and_devices( # Get entity list to verify device_id mapping entities = await client.list_entities_services() - # Collect sensor entities - sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")] + # Collect sensor entities (all entities have device_id) + sensor_entities = entities[0] assert len(sensor_entities) >= 4, ( f"Expected at least 4 sensor entities, got {len(sensor_entities)}" ) diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index 3c5181595f..eaa91ec92e 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio -from aioesphomeapi import EntityState +from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -40,28 +40,22 @@ async def test_device_id_in_state( entity_device_mapping: dict[int, int] = {} for entity in all_entities: - if hasattr(entity, "name") and hasattr(entity, "key"): - if entity.name == "Temperature": - entity_device_mapping[entity.key] = device_ids[ - "Temperature Monitor" - ] - elif entity.name == "Humidity": - entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] - elif entity.name == "Motion Detected": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "Temperature Monitor Power": - entity_device_mapping[entity.key] = device_ids[ - "Temperature Monitor" - ] - elif entity.name == "Temperature Status": - entity_device_mapping[entity.key] = device_ids[ - "Temperature Monitor" - ] - elif entity.name == "Motion Light": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "No Device Sensor": - # Entity without device_id should have device_id 0 - entity_device_mapping[entity.key] = 0 + # All entities have name and key attributes + if entity.name == "Temperature": + entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] + elif entity.name == "Humidity": + entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] + elif entity.name == "Motion Detected": + entity_device_mapping[entity.key] = device_ids["Motion Sensor"] + elif entity.name == "Temperature Monitor Power": + entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] + elif entity.name == "Temperature Status": + entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] + elif entity.name == "Motion Light": + entity_device_mapping[entity.key] = device_ids["Motion Sensor"] + elif entity.name == "No Device Sensor": + # Entity without device_id should have device_id 0 + entity_device_mapping[entity.key] = 0 assert len(entity_device_mapping) >= 6, ( f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}" @@ -111,7 +105,7 @@ async def test_device_id_in_state( ( s for s in states.values() - if hasattr(s, "state") + if isinstance(s, SensorState) and isinstance(s.state, float) and s.device_id != 0 ), @@ -122,11 +116,7 @@ async def test_device_id_in_state( # Find a binary sensor state binary_sensor_state = next( - ( - s - for s in states.values() - if hasattr(s, "state") and isinstance(s.state, bool) - ), + (s for s in states.values() if isinstance(s, BinarySensorState)), None, ) assert binary_sensor_state is not None, "No binary sensor state found" @@ -136,11 +126,7 @@ async def test_device_id_in_state( # Find a text sensor state text_sensor_state = next( - ( - s - for s in states.values() - if hasattr(s, "state") and isinstance(s.state, str) - ), + (s for s in states.values() if isinstance(s, TextSensorState)), None, ) assert text_sensor_state is not None, "No text sensor state found" diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py new file mode 100644 index 0000000000..aec7168165 --- /dev/null +++ b/tests/integration/test_entity_icon.py @@ -0,0 +1,91 @@ +"""Integration test for entity icons with USE_ENTITY_ICON feature.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_entity_icon( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that entities with custom icons work correctly with USE_ENTITY_ICON.""" + # Write, compile and run the ESPHome device, then connect to API + async with run_compiled(yaml_config), api_client_connected() as client: + # Get all entities + entities = await client.list_entities_services() + + # Create a map of entity names to entity info + entity_map = {entity.name: entity for entity in entities[0]} + + # Test entities with icons + icon_test_cases = [ + # (entity_name, expected_icon) + ("Sensor With Icon", "mdi:temperature-celsius"), + ("Binary Sensor With Icon", "mdi:motion-sensor"), + ("Text Sensor With Icon", "mdi:text-box"), + ("Switch With Icon", "mdi:toggle-switch"), + ("Button With Icon", "mdi:gesture-tap-button"), + ("Number With Icon", "mdi:numeric"), + ("Select With Icon", "mdi:format-list-bulleted"), + ] + + # Test entities without icons (should have empty string) + no_icon_test_cases = [ + "Sensor Without Icon", + "Binary Sensor Without Icon", + ] + + # Verify entities with icons + for entity_name, expected_icon in icon_test_cases: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found in API response" + ) + entity = entity_map[entity_name] + + # Check icon field + assert entity.icon == expected_icon, ( + f"{entity_name}: icon mismatch - " + f"expected '{expected_icon}', got '{entity.icon}'" + ) + + # Verify entities without icons + for entity_name in no_icon_test_cases: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found in API response" + ) + entity = entity_map[entity_name] + + # Check icon field is empty + assert entity.icon == "", ( + f"{entity_name}: icon should be empty string for entities without icons, " + f"got '{entity.icon}'" + ) + + # Subscribe to states to ensure everything works normally + states: dict[int, EntityState] = {} + state_received = asyncio.Event() + + def on_state(state: EntityState) -> None: + states[state.key] = state + state_received.set() + + client.subscribe_states(on_state) + + # Wait for states + try: + await asyncio.wait_for(state_received.wait(), timeout=5.0) + except asyncio.TimeoutError: + pytest.fail("No states received within 5 seconds") + + # Verify we received states + assert len(states) > 0, ( + "No states received - entities may not be working correctly" + ) diff --git a/tests/integration/test_host_mode_entity_fields.py b/tests/integration/test_host_mode_entity_fields.py index cf3fa6916a..b9fa3e9746 100644 --- a/tests/integration/test_host_mode_entity_fields.py +++ b/tests/integration/test_host_mode_entity_fields.py @@ -25,8 +25,8 @@ async def test_host_mode_entity_fields( # Create a map of entity names to entity info entity_map = {} for entity in entities[0]: - if hasattr(entity, "name"): - entity_map[entity.name] = entity + # All entities should have a name attribute + entity_map[entity.name] = entity # Test entities that should be visible via API (non-internal) visible_test_cases = [ diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py index 005728b8c6..19d1ee315f 100644 --- a/tests/integration/test_host_mode_many_entities.py +++ b/tests/integration/test_host_mode_many_entities.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio -from aioesphomeapi import EntityState +from aioesphomeapi import EntityState, SensorState import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -30,7 +30,7 @@ async def test_host_mode_many_entities( sensor_states = [ s for s in states.values() - if hasattr(s, "state") and isinstance(s.state, float) + if isinstance(s, SensorState) and isinstance(s.state, float) ] # When we have received states from at least 50 sensors, resolve the future if len(sensor_states) >= 50 and not sensor_count_future.done(): @@ -45,7 +45,7 @@ async def test_host_mode_many_entities( sensor_states = [ s for s in states.values() - if hasattr(s, "state") and isinstance(s.state, float) + if isinstance(s, SensorState) and isinstance(s.state, float) ] pytest.fail( f"Did not receive states from at least 50 sensors within 10 seconds. " @@ -61,7 +61,7 @@ async def test_host_mode_many_entities( sensor_states = [ s for s in states.values() - if hasattr(s, "state") and isinstance(s.state, float) + if isinstance(s, SensorState) and isinstance(s.state, float) ] assert sensor_count >= 50, ( diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py index 049f7db619..8c1e9f5d51 100644 --- a/tests/integration/test_host_mode_sensor.py +++ b/tests/integration/test_host_mode_sensor.py @@ -19,16 +19,17 @@ async def test_host_mode_with_sensor( ) -> None: """Test Host mode with a sensor component.""" # Write, compile and run the ESPHome device, then connect to API + loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: # Subscribe to state changes states: dict[int, EntityState] = {} - sensor_future: asyncio.Future[EntityState] = asyncio.Future() + sensor_future: asyncio.Future[EntityState] = loop.create_future() def on_state(state: EntityState) -> None: states[state.key] = state # If this is our sensor with value 42.0, resolve the future if ( - hasattr(state, "state") + isinstance(state, aioesphomeapi.SensorState) and state.state == 42.0 and not sensor_future.done() ): diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py new file mode 100644 index 0000000000..1c56bbbf9e --- /dev/null +++ b/tests/integration/test_light_calls.py @@ -0,0 +1,190 @@ +"""Integration test for all light call combinations. + +Tests that LightCall handles all possible light operations correctly +including RGB, color temperature, effects, transitions, and flash. +""" + +import asyncio +from typing import Any + +from aioesphomeapi import LightState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_calls( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test all possible LightCall operations and combinations.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Track state changes with futures + state_futures: dict[int, asyncio.Future[Any]] = {} + states: dict[int, Any] = {} + + def on_state(state: Any) -> None: + states[state.key] = state + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + client.subscribe_states(on_state) + + # Get the light entities + entities = await client.list_entities_services() + lights = [e for e in entities[0] if e.object_id.startswith("test_")] + assert len(lights) >= 2 # Should have RGBCW and RGB lights + + rgbcw_light = next(light for light in lights if "RGBCW" in light.name) + rgb_light = next(light for light in lights if "RGB Light" in light.name) + + async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any: + """Wait for a state change for the given entity key.""" + loop = asyncio.get_event_loop() + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + # Test all individual parameters first + + # Test 1: state only + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 2: brightness only + client.light_command(key=rgbcw_light.key, brightness=0.5) + state = await wait_for_state_change(rgbcw_light.key) + assert state.brightness == pytest.approx(0.5) + + # Test 3: color_brightness only + client.light_command(key=rgbcw_light.key, color_brightness=0.8) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_brightness == pytest.approx(0.8) + + # Test 4-7: RGB values must be set together via rgb parameter + client.light_command(key=rgbcw_light.key, rgb=(0.7, 0.3, 0.9)) + state = await wait_for_state_change(rgbcw_light.key) + assert state.red == pytest.approx(0.7, abs=0.1) + assert state.green == pytest.approx(0.3, abs=0.1) + assert state.blue == pytest.approx(0.9, abs=0.1) + + # Test 7: white value + client.light_command(key=rgbcw_light.key, white=0.6) + state = await wait_for_state_change(rgbcw_light.key) + # White might need more tolerance or might not be directly settable + if isinstance(state, LightState) and state.white is not None: + assert state.white == pytest.approx(0.6, abs=0.1) + + # Test 8: color_temperature only + client.light_command(key=rgbcw_light.key, color_temperature=300) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_temperature == pytest.approx(300) + + # Test 9: cold_white only + client.light_command(key=rgbcw_light.key, cold_white=0.8) + state = await wait_for_state_change(rgbcw_light.key) + assert state.cold_white == pytest.approx(0.8) + + # Test 10: warm_white only + client.light_command(key=rgbcw_light.key, warm_white=0.2) + state = await wait_for_state_change(rgbcw_light.key) + assert state.warm_white == pytest.approx(0.2) + + # Test 11: transition_length with state change + client.light_command(key=rgbcw_light.key, state=False, transition_length=0.1) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is False + + # Test 12: flash_length + client.light_command(key=rgbcw_light.key, state=True, flash_length=0.2) + state = await wait_for_state_change(rgbcw_light.key) + # Flash starts + assert state.state is True + # Wait for flash to end + state = await wait_for_state_change(rgbcw_light.key) + + # Test 13: effect only + # First ensure light is on + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + # Now set effect + client.light_command(key=rgbcw_light.key, effect="Random Effect") + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Random Effect" + + # Test 14: stop effect + client.light_command(key=rgbcw_light.key, effect="None") + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "None" + + # Test 15: color_mode parameter + client.light_command( + key=rgbcw_light.key, state=True, color_mode=5 + ) # COLD_WARM_WHITE + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Now test common combinations + + # Test 16: RGB combination (set_rgb) - RGB values get normalized + client.light_command(key=rgbcw_light.key, rgb=(1.0, 0.0, 0.5)) + state = await wait_for_state_change(rgbcw_light.key) + # RGB values get normalized - in this case red is already 1.0 + assert state.red == pytest.approx(1.0, abs=0.1) + assert state.green == pytest.approx(0.0, abs=0.1) + assert state.blue == pytest.approx(0.5, abs=0.1) + + # Test 17: Multiple RGB changes to test transitions + client.light_command(key=rgbcw_light.key, rgb=(0.2, 0.8, 0.4)) + state = await wait_for_state_change(rgbcw_light.key) + # RGB values get normalized so green (highest) becomes 1.0 + # Expected: (0.2/0.8, 0.8/0.8, 0.4/0.8) = (0.25, 1.0, 0.5) + assert state.red == pytest.approx(0.25, abs=0.01) + assert state.green == pytest.approx(1.0, abs=0.01) + assert state.blue == pytest.approx(0.5, abs=0.01) + + # Test 18: State + brightness + transition + client.light_command( + key=rgbcw_light.key, state=True, brightness=0.7, transition_length=0.1 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.7) + + # Test 19: RGB + brightness + color_brightness + client.light_command( + key=rgb_light.key, + state=True, + brightness=0.8, + color_brightness=0.9, + rgb=(0.2, 0.4, 0.6), + ) + state = await wait_for_state_change(rgb_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.8) + + # Test 20: Color temp + cold/warm white + client.light_command( + key=rgbcw_light.key, color_temperature=250, cold_white=0.7, warm_white=0.3 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_temperature == pytest.approx(250) + + # Test 21: Turn RGB light off + client.light_command(key=rgb_light.key, state=False) + state = await wait_for_state_change(rgb_light.key) + assert state.state is False + + # Final cleanup - turn all lights off + for light in lights: + client.light_command( + key=light.key, + state=False, + ) + state = await wait_for_state_change(light.key) + assert state.state is False diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py new file mode 100644 index 0000000000..08ff293b84 --- /dev/null +++ b/tests/integration/test_scheduler_bulk_cleanup.py @@ -0,0 +1,122 @@ +"""Test that triggers the bulk cleanup path when to_remove_ > MAX_LOGICALLY_DELETED_ITEMS.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_bulk_cleanup( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that bulk cleanup path is triggered when many items are cancelled.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_event_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + bulk_cleanup_triggered = False + cleanup_stats: dict[str, int] = { + "removed": 0, + "before": 0, + "after": 0, + } + post_cleanup_executed = 0 + + def on_log_line(line: str) -> None: + nonlocal bulk_cleanup_triggered, post_cleanup_executed + + # Look for logs indicating bulk cleanup was triggered + # The actual cleanup happens silently, so we track the cancel operations + if "Successfully cancelled" in line and "timeouts" in line: + match = re.search(r"Successfully cancelled (\d+) timeouts", line) + if match and int(match.group(1)) > 10: + bulk_cleanup_triggered = True + + # Track cleanup statistics + match = re.search(r"Bulk cleanup triggered: removed (\d+) items", line) + if match: + cleanup_stats["removed"] = int(match.group(1)) + + match = re.search(r"Items before cleanup: (\d+), after: (\d+)", line) + if match: + cleanup_stats["before"] = int(match.group(1)) + cleanup_stats["after"] = int(match.group(2)) + + # Track post-cleanup timeout executions + if "Post-cleanup timeout" in line and "executed correctly" in line: + match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line) + if match: + post_cleanup_executed += 1 + + # Check for final test completion + if ( + "All post-cleanup timeouts completed - test finished" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-bulk-cleanup" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + trigger_bulk_cleanup_service: UserService | None = None + for service in services: + if service.name == "trigger_bulk_cleanup": + trigger_bulk_cleanup_service = service + break + + assert trigger_bulk_cleanup_service is not None, ( + "trigger_bulk_cleanup service not found" + ) + + # Execute the test + client.execute_service(trigger_bulk_cleanup_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail("Bulk cleanup test timed out") + + # Verify bulk cleanup was triggered + assert bulk_cleanup_triggered, ( + "Bulk cleanup path was not triggered - MAX_LOGICALLY_DELETED_ITEMS threshold not reached" + ) + + # Verify cleanup statistics + assert cleanup_stats["removed"] > 10, ( + f"Expected more than 10 items removed, got {cleanup_stats['removed']}" + ) + + # Verify scheduler still works after bulk cleanup + assert post_cleanup_executed == 5, ( + f"Expected 5 post-cleanup timeouts to execute, but {post_cleanup_executed} executed" + ) diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py new file mode 100644 index 0000000000..923cf946c4 --- /dev/null +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -0,0 +1,94 @@ +"""Test that defer() with the same name cancels previous defers.""" + +import asyncio + +from aioesphomeapi import EntityState, Event, EventInfo, UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_defer_cancel( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that defer() with the same name cancels previous defers.""" + + async with run_compiled(yaml_config), api_client_connected() as client: + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-defer-cancel" + + # List entities and services + entity_info, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test entities + test_complete_entity: EventInfo | None = None + test_result_entity: EventInfo | None = None + + for entity in entity_info: + if isinstance(entity, EventInfo): + if entity.object_id == "test_complete": + test_complete_entity = entity + elif entity.object_id == "test_result": + test_result_entity = entity + + assert test_complete_entity is not None, "test_complete event not found" + assert test_result_entity is not None, "test_result event not found" + + # Find our test service + test_defer_cancel_service: UserService | None = None + for service in services: + if service.name == "test_defer_cancel": + test_defer_cancel_service = service + + assert test_defer_cancel_service is not None, ( + "test_defer_cancel service not found" + ) + + # Get the event loop + loop = asyncio.get_running_loop() + + # Subscribe to states + test_complete_future: asyncio.Future[bool] = loop.create_future() + test_result_future: asyncio.Future[int] = loop.create_future() + + def on_state(state: EntityState) -> None: + if not isinstance(state, Event): + return + + if ( + state.key == test_complete_entity.key + and state.event_type == "test_finished" + and not test_complete_future.done() + ): + test_complete_future.set_result(True) + return + + if state.key == test_result_entity.key and not test_result_future.done(): + # Event type should be "defer_executed_X" where X is the defer number + if state.event_type.startswith("defer_executed_"): + defer_num = int(state.event_type.split("_")[-1]) + test_result_future.set_result(defer_num) + + client.subscribe_states(on_state) + + # Execute the test + client.execute_service(test_defer_cancel_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Test did not complete within timeout") + + # Verify that only defer 10 was executed + assert executed_defer == 10, ( + f"Expected defer 10 to execute, got {executed_defer}" + ) diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py new file mode 100644 index 0000000000..57b7134feb --- /dev/null +++ b/tests/integration/test_scheduler_defer_cancel_regular.py @@ -0,0 +1,90 @@ +"""Test that a deferred timeout cancels a regular timeout with the same name.""" + +import asyncio + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_defer_cancels_regular( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that set_timeout(name, 0) cancels a previously scheduled set_timeout(name, delay).""" + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track log messages + log_messages: list[str] = [] + error_detected = False + + def on_log_line(line: str) -> None: + nonlocal error_detected + if "TEST" in line: + log_messages.append(line) + + if "ERROR: Regular timeout executed" in line: + error_detected = True + + if "Test complete" in line and not test_complete_future.done(): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-defer-cancel-regular" + + # List services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + test_service: UserService | None = None + for service in services: + if service.name == "test_defer_cancels_regular": + test_service = service + break + + assert test_service is not None, "test_defer_cancels_regular service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=5.0) + except asyncio.TimeoutError: + pytest.fail(f"Test timed out. Log messages: {log_messages}") + + # Verify results + assert not error_detected, ( + f"Regular timeout should have been cancelled but it executed! Logs: {log_messages}" + ) + + # Verify the deferred timeout executed + assert any( + "SUCCESS: Deferred timeout executed" in msg for msg in log_messages + ), f"Deferred timeout should have executed. Logs: {log_messages}" + + # Verify the expected sequence of events + assert any( + "Starting defer cancels regular timeout test" in msg for msg in log_messages + ) + assert any( + "Scheduled regular timeout with 100ms delay" in msg for msg in log_messages + ) + assert any( + "Scheduled deferred timeout - should cancel regular timeout" in msg + for msg in log_messages + ) diff --git a/tests/integration/test_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py similarity index 97% rename from tests/integration/test_defer_fifo_simple.py rename to tests/integration/test_scheduler_defer_fifo_simple.py index 5a62a45786..eb4058fedd 100644 --- a/tests/integration/test_defer_fifo_simple.py +++ b/tests/integration/test_scheduler_defer_fifo_simple.py @@ -9,7 +9,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction @pytest.mark.asyncio -async def test_defer_fifo_simple( +async def test_scheduler_defer_fifo_simple( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, @@ -20,7 +20,7 @@ async def test_defer_fifo_simple( # Verify we can connect device_info = await client.device_info() assert device_info is not None - assert device_info.name == "defer-fifo-simple" + assert device_info.name == "scheduler-defer-fifo-simple" # List entities and services entity_info, services = await asyncio.wait_for( diff --git a/tests/integration/test_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py similarity index 97% rename from tests/integration/test_defer_stress.py rename to tests/integration/test_scheduler_defer_stress.py index f63ec8d25f..d546b7132f 100644 --- a/tests/integration/test_defer_stress.py +++ b/tests/integration/test_scheduler_defer_stress.py @@ -11,7 +11,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction @pytest.mark.asyncio -async def test_defer_stress( +async def test_scheduler_defer_stress( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, @@ -75,7 +75,7 @@ async def test_defer_stress( # Verify we can connect device_info = await client.device_info() assert device_info is not None - assert device_info.name == "defer-stress-test" + assert device_info.name == "scheduler-defer-stress-test" # List entities and services entity_info, services = await asyncio.wait_for( diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py new file mode 100644 index 0000000000..3c757bfc9d --- /dev/null +++ b/tests/integration/test_scheduler_heap_stress.py @@ -0,0 +1,140 @@ +"""Stress test for heap scheduler thread safety with multiple threads.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_heap_stress( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that set_timeout/set_interval doesn't crash when called rapidly from multiple threads.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track executed timeouts/intervals and their order + executed_callbacks: set[int] = set() + thread_executions: dict[ + int, list[int] + ] = {} # thread_id -> list of indices in execution order + callback_types: dict[int, str] = {} # callback_id -> "timeout" or "interval" + + def on_log_line(line: str) -> None: + # Track all executed callbacks with thread and index info + match = re.search( + r"Executed (timeout|interval) (\d+) \(thread (\d+), index (\d+)\)", line + ) + if not match: + # Also check for the completion message + if "All threads finished" in line and "Created 1000 callbacks" in line: + # Give scheduler some time to execute callbacks + pass + return + + callback_type = match.group(1) + callback_id = int(match.group(2)) + thread_id = int(match.group(3)) + index = int(match.group(4)) + + # Only count each callback ID once (intervals might fire multiple times) + if callback_id not in executed_callbacks: + executed_callbacks.add(callback_id) + callback_types[callback_id] = callback_type + + # Track execution order per thread + if thread_id not in thread_executions: + thread_executions[thread_id] = [] + + # Only append if this is a new execution for this thread + if index not in thread_executions[thread_id]: + thread_executions[thread_id].append(index) + + # Check if we've executed all 1000 callbacks (0-999) + if len(executed_callbacks) >= 1000 and not test_complete_future.done(): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-heap-stress-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_stress_test_service: UserService | None = None + for service in services: + if service.name == "run_heap_stress_test": + run_stress_test_service = service + break + + assert run_stress_test_service is not None, ( + "run_heap_stress_test service not found" + ) + + # Call the run_heap_stress_test service to start the test + client.execute_service(run_stress_test_service, {}) + + # Wait for all callbacks to execute (should be quick, but give more time for scheduling) + try: + await asyncio.wait_for(test_complete_future, timeout=60.0) + except asyncio.TimeoutError: + # Report how many we got + pytest.fail( + f"Stress test timed out. Only {len(executed_callbacks)} of " + f"1000 callbacks executed. Missing IDs: " + f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..." + ) + + # Verify all callbacks executed + assert len(executed_callbacks) == 1000, ( + f"Expected 1000 callbacks, got {len(executed_callbacks)}" + ) + + # Verify we have all IDs from 0-999 + expected_ids = set(range(1000)) + missing_ids = expected_ids - executed_callbacks + assert not missing_ids, f"Missing callback IDs: {sorted(missing_ids)}" + + # Verify we have a mix of timeouts and intervals + timeout_count = sum(1 for t in callback_types.values() if t == "timeout") + interval_count = sum(1 for t in callback_types.values() if t == "interval") + assert timeout_count > 0, "No timeouts were executed" + assert interval_count > 0, "No intervals were executed" + + # Verify each thread executed callbacks + for thread_id, indices in thread_executions.items(): + assert len(indices) == 100, ( + f"Thread {thread_id} executed {len(indices)} callbacks, expected 100" + ) + # Total should be 1000 callbacks + total_callbacks = timeout_count + interval_count + assert total_callbacks == 1000, ( + f"Expected 1000 total callbacks but got {total_callbacks}" + ) diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py new file mode 100644 index 0000000000..90577f36f1 --- /dev/null +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -0,0 +1,142 @@ +"""Rapid cancellation test - schedule and immediately cancel timeouts with string names.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_rapid_cancellation( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test rapid schedule/cancel cycles that might expose race conditions.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track test progress + test_stats = { + "log_count": 0, + "errors": [], + "summary_scheduled": None, + "final_scheduled": 0, + "final_executed": 0, + "final_implicit_cancellations": 0, + } + + def on_log_line(line: str) -> None: + # Count log lines + test_stats["log_count"] += 1 + + # Check for errors (only ERROR level, not WARN) + if "ERROR" in line: + test_stats["errors"].append(line) + + # Parse summary statistics + if "All threads completed. Scheduled:" in line: + # Extract the scheduled count from the summary + if match := re.search(r"Scheduled: (\d+)", line): + test_stats["summary_scheduled"] = int(match.group(1)) + elif "Total scheduled:" in line: + if match := re.search(r"Total scheduled: (\d+)", line): + test_stats["final_scheduled"] = int(match.group(1)) + elif "Total executed:" in line: + if match := re.search(r"Total executed: (\d+)", line): + test_stats["final_executed"] = int(match.group(1)) + elif "Implicit cancellations (replaced):" in line: + if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line): + test_stats["final_implicit_cancellations"] = int(match.group(1)) + + # Check for crash indicators + if any( + indicator in line.lower() + for indicator in ["segfault", "abort", "assertion", "heap corruption"] + ): + if not test_complete_future.done(): + test_complete_future.set_exception(Exception(f"Crash detected: {line}")) + return + + # Check for completion - wait for final message after all stats are logged + if ( + "Test finished - all statistics reported" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-rapid-cancel-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_test_service: UserService | None = None + for service in services: + if service.name == "run_rapid_cancellation_test": + run_test_service = service + break + + assert run_test_service is not None, ( + "run_rapid_cancellation_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_test_service, {}) + + # Wait for test to complete with timeout + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail(f"Test timed out. Stats: {test_stats}") + + # Check for any errors + assert len(test_stats["errors"]) == 0, ( + f"Errors detected: {test_stats['errors']}" + ) + + # Check that we received log messages + assert test_stats["log_count"] > 0, "No log messages received" + + # Check the summary line to verify all threads scheduled their operations + assert test_stats["summary_scheduled"] == 400, ( + f"Expected summary to show 400 scheduled operations but got {test_stats['summary_scheduled']}" + ) + + # Check final statistics + assert test_stats["final_scheduled"] == 400, ( + f"Expected final stats to show 400 scheduled but got {test_stats['final_scheduled']}" + ) + + assert test_stats["final_executed"] == 10, ( + f"Expected final stats to show 10 executed but got {test_stats['final_executed']}" + ) + + assert test_stats["final_implicit_cancellations"] == 390, ( + f"Expected final stats to show 390 implicit cancellations but got {test_stats['final_implicit_cancellations']}" + ) diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py new file mode 100644 index 0000000000..c015978e15 --- /dev/null +++ b/tests/integration/test_scheduler_recursive_timeout.py @@ -0,0 +1,101 @@ +"""Test for recursive timeout scheduling - scheduling timeouts from within timeout callbacks.""" + +import asyncio +from pathlib import Path + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_recursive_timeout( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduling timeouts from within timeout callbacks works correctly.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track execution sequence + execution_sequence: list[str] = [] + expected_sequence = [ + "initial_timeout", + "nested_timeout_1", + "nested_timeout_2", + "test_complete", + ] + + def on_log_line(line: str) -> None: + # Track execution sequence + if "Executing initial timeout" in line: + execution_sequence.append("initial_timeout") + elif "Executing nested timeout 1" in line: + execution_sequence.append("nested_timeout_1") + elif "Executing nested timeout 2" in line: + execution_sequence.append("nested_timeout_2") + elif "Recursive timeout test complete" in line: + execution_sequence.append("test_complete") + if not test_complete_future.done(): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-recursive-timeout" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_test_service: UserService | None = None + for service in services: + if service.name == "run_recursive_timeout_test": + run_test_service = service + break + + assert run_test_service is not None, ( + "run_recursive_timeout_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"Recursive timeout test timed out. Got sequence: {execution_sequence}" + ) + + # Verify execution sequence + assert execution_sequence == expected_sequence, ( + f"Execution sequence mismatch. Expected {expected_sequence}, " + f"got {execution_sequence}" + ) + + # Verify we got exactly 4 events (Initial + Level 1 + Level 2 + Complete) + assert len(execution_sequence) == 4, ( + f"Expected 4 events but got {len(execution_sequence)}" + ) diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py new file mode 100644 index 0000000000..f5120ce4ce --- /dev/null +++ b/tests/integration/test_scheduler_simultaneous_callbacks.py @@ -0,0 +1,123 @@ +"""Simultaneous callbacks test - schedule many callbacks for the same time from multiple threads.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_simultaneous_callbacks( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test scheduling many callbacks for the exact same time from multiple threads.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track test progress + test_stats = { + "scheduled": 0, + "executed": 0, + "expected": 1000, # 10 threads * 100 callbacks + "errors": [], + } + + def on_log_line(line: str) -> None: + # Track operations + if "Scheduled callback" in line: + test_stats["scheduled"] += 1 + elif "Callback executed" in line: + test_stats["executed"] += 1 + elif "ERROR" in line: + test_stats["errors"].append(line) + + # Check for crash indicators + if any( + indicator in line.lower() + for indicator in ["segfault", "abort", "assertion", "heap corruption"] + ): + if not test_complete_future.done(): + test_complete_future.set_exception(Exception(f"Crash detected: {line}")) + return + + # Check for completion with final count + if "Final executed count:" in line: + # Extract number from log line like: "[07:59:47][I][simultaneous_callbacks:093]: Simultaneous callbacks test complete. Final executed count: 1000" + match = re.search(r"Final executed count:\s*(\d+)", line) + if match: + test_stats["final_count"] = int(match.group(1)) + + # Check for completion + if ( + "Simultaneous callbacks test complete" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-simul-callbacks-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_test_service: UserService | None = None + for service in services: + if service.name == "run_simultaneous_callbacks_test": + run_test_service = service + break + + assert run_test_service is not None, ( + "run_simultaneous_callbacks_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete_future, timeout=30.0) + except asyncio.TimeoutError: + pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}") + + # Check for any errors + assert len(test_stats["errors"]) == 0, ( + f"Errors detected: {test_stats['errors']}" + ) + + # Verify all callbacks executed using the final count from C++ + final_count = test_stats.get("final_count", 0) + assert final_count == test_stats["expected"], ( + f"Expected {test_stats['expected']} callbacks, but only {final_count} executed" + ) + + # The final_count is the authoritative count from the C++ component + assert final_count == 1000, ( + f"Expected 1000 executed callbacks but got {final_count}" + ) diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py new file mode 100644 index 0000000000..4d77abd954 --- /dev/null +++ b/tests/integration/test_scheduler_string_lifetime.py @@ -0,0 +1,169 @@ +"""String lifetime test - verify scheduler handles string destruction correctly.""" + +import asyncio +from pathlib import Path +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_lifetime( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler correctly handles string lifetimes when strings go out of scope.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create events for synchronization + test1_complete = asyncio.Event() + test2_complete = asyncio.Event() + test3_complete = asyncio.Event() + test4_complete = asyncio.Event() + test5_complete = asyncio.Event() + all_tests_complete = asyncio.Event() + + # Track test progress + test_stats = { + "tests_passed": 0, + "tests_failed": 0, + "errors": [], + "current_test": None, + "test_callbacks_executed": {}, + } + + def on_log_line(line: str) -> None: + # Track test-specific events + if "Test 1 complete" in line: + test1_complete.set() + elif "Test 2 complete" in line: + test2_complete.set() + elif "Test 3 complete" in line: + test3_complete.set() + elif "Test 4 complete" in line: + test4_complete.set() + elif "Test 5 complete" in line: + test5_complete.set() + + # Track individual callback executions + callback_match = re.search(r"Callback '(.+?)' executed", line) + if callback_match: + callback_name = callback_match.group(1) + test_stats["test_callbacks_executed"][callback_name] = True + + # Track test results from the C++ test output + if "Tests passed:" in line and "string_lifetime" in line: + # Extract the number from "Tests passed: 32" + match = re.search(r"Tests passed:\s*(\d+)", line) + if match: + test_stats["tests_passed"] = int(match.group(1)) + elif "Tests failed:" in line and "string_lifetime" in line: + match = re.search(r"Tests failed:\s*(\d+)", line) + if match: + test_stats["tests_failed"] = int(match.group(1)) + elif "ERROR" in line and "string_lifetime" in line: + test_stats["errors"].append(line) + + # Check for memory corruption indicators + if any( + indicator in line.lower() + for indicator in [ + "use after free", + "heap corruption", + "segfault", + "abort", + "assertion", + "sanitizer", + "bad memory", + "invalid pointer", + ] + ): + pytest.fail(f"Memory corruption detected: {line}") + + # Check for completion + if "String lifetime tests complete" in line: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-string-lifetime-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test services + test_services = {} + for service in services: + if service.name == "run_test1": + test_services["test1"] = service + elif service.name == "run_test2": + test_services["test2"] = service + elif service.name == "run_test3": + test_services["test3"] = service + elif service.name == "run_test4": + test_services["test4"] = service + elif service.name == "run_test5": + test_services["test5"] = service + elif service.name == "run_final_check": + test_services["final"] = service + + # Ensure all services are found + required_services = ["test1", "test2", "test3", "test4", "test5", "final"] + for service_name in required_services: + assert service_name in test_services, f"{service_name} service not found" + + # Run tests sequentially, waiting for each to complete + try: + # Test 1 + client.execute_service(test_services["test1"], {}) + await asyncio.wait_for(test1_complete.wait(), timeout=5.0) + + # Test 2 + client.execute_service(test_services["test2"], {}) + await asyncio.wait_for(test2_complete.wait(), timeout=5.0) + + # Test 3 + client.execute_service(test_services["test3"], {}) + await asyncio.wait_for(test3_complete.wait(), timeout=5.0) + + # Test 4 + client.execute_service(test_services["test4"], {}) + await asyncio.wait_for(test4_complete.wait(), timeout=5.0) + + # Test 5 + client.execute_service(test_services["test5"], {}) + await asyncio.wait_for(test5_complete.wait(), timeout=5.0) + + # Final check + client.execute_service(test_services["final"], {}) + await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) + + except asyncio.TimeoutError: + pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") + + # Check for any errors + assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}" + + # Verify we had the expected number of passing tests + assert test_stats["tests_passed"] == 30, ( + f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}" + ) diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py new file mode 100644 index 0000000000..3045842223 --- /dev/null +++ b/tests/integration/test_scheduler_string_name_stress.py @@ -0,0 +1,116 @@ +"""Stress test for heap scheduler with std::string names from multiple threads.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_name_stress( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track executed callbacks and any crashes + executed_callbacks: set[int] = set() + error_messages: list[str] = [] + + def on_log_line(line: str) -> None: + # Check for crash indicators + if any( + indicator in line.lower() + for indicator in [ + "segfault", + "abort", + "assertion", + "heap corruption", + "use after free", + ] + ): + error_messages.append(line) + if not test_complete_future.done(): + test_complete_future.set_exception(Exception(f"Crash detected: {line}")) + return + + # Track executed callbacks + match = re.search(r"Executed string-named callback (\d+)", line) + if match: + callback_id = int(match.group(1)) + executed_callbacks.add(callback_id) + + # Check for completion + if ( + "String name stress test complete" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-string-name-stress" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_stress_test_service: UserService | None = None + for service in services: + if service.name == "run_string_name_stress_test": + run_stress_test_service = service + break + + assert run_stress_test_service is not None, ( + "run_string_name_stress_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_stress_test_service, {}) + + # Wait for test to complete or crash + try: + await asyncio.wait_for(test_complete_future, timeout=30.0) + except asyncio.TimeoutError: + pytest.fail( + f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " + f"This might indicate a deadlock." + ) + + # Verify no errors occurred (crashes already handled by exception) + assert not error_messages, f"Errors detected during test: {error_messages}" + + # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each) + assert len(executed_callbacks) == 1000, ( + f"Expected 1000 callbacks but got {len(executed_callbacks)}" + ) + + # Verify each callback ID was executed exactly once + for i in range(1000): + assert i in executed_callbacks, f"Callback {i} was not executed" diff --git a/tests/unit_tests/test_config_helpers.py b/tests/unit_tests/test_config_helpers.py new file mode 100644 index 0000000000..1c850e3759 --- /dev/null +++ b/tests/unit_tests/test_config_helpers.py @@ -0,0 +1,135 @@ +"""Unit tests for esphome.config_helpers module.""" + +from collections.abc import Callable +from unittest.mock import patch + +from esphome.config_helpers import filter_source_files_from_platform, get_logger_level +from esphome.const import ( + CONF_LEVEL, + CONF_LOGGER, + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PlatformFramework, +) + + +def test_filter_source_files_from_platform_esp32() -> None: + """Test that filter_source_files_from_platform correctly filters files for ESP32 platform.""" + # Define test file mappings + files_map: dict[str, set[PlatformFramework]] = { + "logger_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + "logger_common.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.HOST_NATIVE, + }, + } + + # Create the filter function + filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map) + + # Test ESP32 with Arduino framework + mock_core_data: dict[str, dict[str, str]] = { + KEY_CORE: { + KEY_TARGET_PLATFORM: "esp32", + KEY_TARGET_FRAMEWORK: "arduino", + } + } + + with patch("esphome.config_helpers.CORE.data", mock_core_data): + excluded: list[str] = filter_func() + # ESP32 Arduino should exclude ESP8266 and HOST files + assert "logger_esp8266.cpp" in excluded + assert "logger_host.cpp" in excluded + # But not ESP32 or common files + assert "logger_esp32.cpp" not in excluded + assert "logger_common.cpp" not in excluded + + +def test_filter_source_files_from_platform_host() -> None: + """Test that filter_source_files_from_platform correctly filters files for HOST platform.""" + # Define test file mappings + files_map: dict[str, set[PlatformFramework]] = { + "logger_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + "logger_common.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.HOST_NATIVE, + }, + } + + # Create the filter function + filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map) + + # Test Host platform + mock_core_data: dict[str, dict[str, str]] = { + KEY_CORE: { + KEY_TARGET_PLATFORM: "host", + KEY_TARGET_FRAMEWORK: "host", # Framework.NATIVE is "host" + } + } + + with patch("esphome.config_helpers.CORE.data", mock_core_data): + excluded: list[str] = filter_func() + # Host should exclude ESP32 and ESP8266 files + assert "logger_esp32.cpp" in excluded + assert "logger_esp8266.cpp" in excluded + # But not host or common files + assert "logger_host.cpp" not in excluded + assert "logger_common.cpp" not in excluded + + +def test_filter_source_files_from_platform_handles_missing_data() -> None: + """Test that filter_source_files_from_platform returns empty list when platform/framework data is missing.""" + # Define test file mappings + files_map: dict[str, set[PlatformFramework]] = { + "logger_esp32.cpp": {PlatformFramework.ESP32_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + } + + # Create the filter function + filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map) + + # Test case: Missing platform/framework data + mock_core_data: dict[str, dict[str, str]] = {KEY_CORE: {}} + + with patch("esphome.config_helpers.CORE.data", mock_core_data): + excluded: list[str] = filter_func() + # Should return empty list when platform/framework not set + assert excluded == [] + + +def test_get_logger_level() -> None: + """Test get_logger_level helper function.""" + # Test no logger config - should return default DEBUG + mock_config = {} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "DEBUG" + + # Test with logger set to INFO + mock_config = {CONF_LOGGER: {CONF_LEVEL: "INFO"}} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "INFO" + + # Test with VERY_VERBOSE + mock_config = {CONF_LOGGER: {CONF_LEVEL: "VERY_VERBOSE"}} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "VERY_VERBOSE" + + # Test with logger missing level (uses default DEBUG) + mock_config = {CONF_LOGGER: {}} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "DEBUG" diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py new file mode 100644 index 0000000000..c6d4c4aef0 --- /dev/null +++ b/tests/unit_tests/test_loader.py @@ -0,0 +1,63 @@ +"""Unit tests for esphome.loader module.""" + +from unittest.mock import MagicMock, patch + +from esphome.loader import ComponentManifest + + +def test_component_manifest_resources_with_filter_source_files() -> None: + """Test that ComponentManifest.resources correctly filters out excluded files.""" + # Create a mock module with FILTER_SOURCE_FILES function + mock_module = MagicMock() + mock_module.FILTER_SOURCE_FILES = lambda: [ + "platform_esp32.cpp", + "platform_esp8266.cpp", + ] + mock_module.__package__ = "esphome.components.test_component" + + # Create ComponentManifest instance + manifest = ComponentManifest(mock_module) + + # Mock the files in the package + def create_mock_file(filename: str) -> MagicMock: + mock_file = MagicMock() + mock_file.name = filename + mock_file.is_file.return_value = True + return mock_file + + mock_files = [ + create_mock_file("test.cpp"), + create_mock_file("test.h"), + create_mock_file("platform_esp32.cpp"), + create_mock_file("platform_esp8266.cpp"), + create_mock_file("common.cpp"), + create_mock_file("README.md"), # Should be excluded by extension + ] + + # Mock importlib.resources + with patch("importlib.resources.files") as mock_files_func: + mock_package_files = MagicMock() + mock_package_files.iterdir.return_value = mock_files + mock_package_files.joinpath = lambda name: MagicMock(is_file=lambda: True) + mock_files_func.return_value = mock_package_files + + # Get resources + resources = manifest.resources + + # Convert to list of filenames for easier testing + resource_names = [r.resource for r in resources] + + # Check that platform files are excluded + assert "platform_esp32.cpp" not in resource_names + assert "platform_esp8266.cpp" not in resource_names + + # Check that other source files are included + assert "test.cpp" in resource_names + assert "test.h" in resource_names + assert "common.cpp" in resource_names + + # Check that non-source files are excluded + assert "README.md" not in resource_names + + # Verify the correct number of resources + assert len(resources) == 3 # test.cpp, test.h, common.cpp