diff --git a/.clang-tidy.hash b/.clang-tidy.hash index e1f5e096c0..a14b44ef96 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -5ac05ac603766d76b86a05cdf6a43febcaae807fe9e2406d812c47d4b5fed91d +94557f94be073390342833aff12ef8676a8b597db5fa770a5a1232e9425cb48f diff --git a/CODEOWNERS b/CODEOWNERS index f95d68a46d..0d9396aa6f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -91,6 +91,7 @@ esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp581/* @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid +esphome/components/bthome_mithermometer/* @nagyrobi esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow esphome/components/camera/* @bdraco @DT-art1 diff --git a/MANIFEST.in b/MANIFEST.in index 45d5e86672..ed65edc656 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include LICENSE include README.md include requirements.txt +recursive-include esphome *.yaml recursive-include esphome *.cpp *.h *.tcc *.c recursive-include esphome *.py.script recursive-include esphome LICENSE.txt diff --git a/docker/Dockerfile b/docker/Dockerfile index 64ce67e819..348a503bc8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base RUN git config --system --add safe.directory "*" +# Install build tools for Python packages that require compilation +# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) +RUN if command -v apk > /dev/null; then \ + apk add --no-cache build-base; \ + else \ + apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/*; \ + fi + ENV PIP_DISABLE_PIP_VERSION_CHECK=1 RUN pip install --no-cache-dir -U pip uv==0.6.14 diff --git a/esphome/__main__.py b/esphome/__main__.py index 119ab957a3..3822af0330 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -789,7 +789,13 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info("Successfully compiled program.") + if CORE.is_host: + from esphome.platformio_api import get_idedata + + program_path = str(get_idedata(config).firmware_elf_path) + _LOGGER.info("Successfully compiled program to path '%s'", program_path) + else: + _LOGGER.info("Successfully compiled program.") return 0 @@ -839,10 +845,8 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: if CORE.is_host: from esphome.platformio_api import get_idedata - idedata = get_idedata(config) - if idedata is None: - return 1 - program_path = idedata.raw["prog_path"] + program_path = str(get_idedata(config).firmware_elf_path) + _LOGGER.info("Running program from path '%s'", program_path) return run_external_process(program_path) # Get devices, resolving special identifiers like OTA diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c351bc8c9c..debea5808c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -102,7 +102,7 @@ message HelloRequest { // For example "Home Assistant" // Not strictly necessary to send but nice for debugging // purposes. - string client_info = 1 [(pointer_to_buffer) = true]; + string client_info = 1; uint32 api_version_major = 2; uint32 api_version_minor = 3; } @@ -139,7 +139,7 @@ message AuthenticationRequest { option (ifdef) = "USE_API_PASSWORD"; // The password to log in with - string password = 1 [(pointer_to_buffer) = true]; + string password = 1; } // Confirmation of successful connection. After this the connection is available for all traffic. @@ -477,7 +477,7 @@ message FanCommandRequest { bool has_speed_level = 10; int32 speed_level = 11; bool has_preset_mode = 12; - string preset_mode = 13 [(pointer_to_buffer) = true]; + string preset_mode = 13; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } @@ -579,7 +579,7 @@ message LightCommandRequest { bool has_flash_length = 16; uint32 flash_length = 17; bool has_effect = 18; - string effect = 19 [(pointer_to_buffer) = true]; + string effect = 19; uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"]; } @@ -824,9 +824,9 @@ message HomeAssistantStateResponse { option (no_delay) = true; option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; - string entity_id = 1 [(pointer_to_buffer) = true]; - string state = 2 [(pointer_to_buffer) = true]; - string attribute = 3 [(pointer_to_buffer) = true]; + string entity_id = 1; + string state = 2; + string attribute = 3; } // ==================== IMPORT TIME ==================== @@ -841,7 +841,7 @@ message GetTimeResponse { option (no_delay) = true; fixed32 epoch_seconds = 1; - string timezone = 2 [(pointer_to_buffer) = true]; + string timezone = 2; } // ==================== USER-DEFINES SERVICES ==================== @@ -1091,11 +1091,11 @@ message ClimateCommandRequest { bool has_swing_mode = 14; ClimateSwingMode swing_mode = 15; bool has_custom_fan_mode = 16; - string custom_fan_mode = 17 [(pointer_to_buffer) = true]; + string custom_fan_mode = 17; bool has_preset = 18; ClimatePreset preset = 19; bool has_custom_preset = 20; - string custom_preset = 21 [(pointer_to_buffer) = true]; + string custom_preset = 21; bool has_target_humidity = 22; float target_humidity = 23; uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; @@ -1274,7 +1274,7 @@ message SelectCommandRequest { option (base_class) = "CommandProtoMessage"; fixed32 key = 1; - string state = 2 [(pointer_to_buffer) = true]; + string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b5628f654e..26ddb16e9a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -473,7 +473,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (msg.has_direction) call.set_direction(static_cast(msg.direction)); if (msg.has_preset_mode) - call.set_preset_mode(reinterpret_cast(msg.preset_mode), msg.preset_mode_len); + call.set_preset_mode(msg.preset_mode.c_str(), msg.preset_mode.size()); call.perform(); } #endif @@ -559,7 +559,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) { if (msg.has_flash_length) call.set_flash_length(msg.flash_length); if (msg.has_effect) - call.set_effect(reinterpret_cast(msg.effect), msg.effect_len); + call.set_effect(msg.effect.c_str(), msg.effect.size()); call.perform(); } #endif @@ -738,11 +738,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) - call.set_fan_mode(reinterpret_cast(msg.custom_fan_mode), msg.custom_fan_mode_len); + call.set_fan_mode(msg.custom_fan_mode.c_str(), msg.custom_fan_mode.size()); if (msg.has_preset) call.set_preset(static_cast(msg.preset)); if (msg.has_custom_preset) - call.set_preset(reinterpret_cast(msg.custom_preset), msg.custom_preset_len); + call.set_preset(msg.custom_preset.c_str(), msg.custom_preset.size()); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); @@ -931,7 +931,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection * } void APIConnection::select_command(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) - call.set_option(reinterpret_cast(msg.state), msg.state_len); + call.set_option(msg.state.c_str(), msg.state.size()); call.perform(); } #endif @@ -1153,9 +1153,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE - if (value.timezone_len > 0) { - homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast(value.timezone), - value.timezone_len); + if (!value.timezone.empty()) { + homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); } #endif } @@ -1522,7 +1521,7 @@ void APIConnection::complete_authentication_() { } bool APIConnection::send_hello_response(const HelloRequest &msg) { - this->client_info_.name.assign(reinterpret_cast(msg.client_info), msg.client_info_len); + this->client_info_.name.assign(msg.client_info.c_str(), msg.client_info.size()); this->client_info_.peername = this->helper_->getpeername(); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; @@ -1550,7 +1549,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) { AuthenticationResponse resp; // bool invalid_password = 1; - resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len); + resp.invalid_password = !this->parent_->check_password(msg.password.byte(), msg.password.size()); if (!resp.invalid_password) { this->complete_authentication_(); } @@ -1693,27 +1692,28 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { // Skip if entity_id is empty (invalid message) - if (msg.entity_id_len == 0) { + if (msg.entity_id.empty()) { return; } for (auto &it : this->parent_->get_state_subs()) { // Compare entity_id: check length matches and content matches size_t entity_id_len = strlen(it.entity_id); - if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) { + if (entity_id_len != msg.entity_id.size() || + memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) { continue; } // Compare attribute: either both have matching attribute, or both have none size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0; - if (sub_attr_len != msg.attribute_len || - (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) { + if (sub_attr_len != msg.attribute.size() || + (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) { continue; } // Create temporary string for callback (callback takes const std::string &) - // Handle empty state (nullptr with len=0) - std::string state(msg.state_len > 0 ? reinterpret_cast(msg.state) : "", msg.state_len); + // Handle empty state + std::string state(!msg.state.empty() ? msg.state.c_str() : "", msg.state.size()); it.callback(state); } } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3376b022c5..edd6dfc6a9 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -23,9 +23,7 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation - this->client_info = value.data(); - this->client_info_len = value.size(); + this->client_info = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -49,9 +47,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const { bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation - this->password = value.data(); - this->password_len = value.size(); + this->password = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -448,9 +444,7 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 13: { - // Use raw data directly to avoid allocation - this->preset_mode = value.data(); - this->preset_mode_len = value.size(); + this->preset_mode = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -615,9 +609,7 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 19: { - // Use raw data directly to avoid allocation - this->effect = value.data(); - this->effect_len = value.size(); + this->effect = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -859,7 +851,6 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation this->key = value.data(); this->key_len = value.size(); break; @@ -936,12 +927,12 @@ bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt v } bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: - this->error_message = value.as_string(); + case 3: { + this->error_message = StringRef(reinterpret_cast(value.data()), value.size()); break; + } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON case 4: { - // Use raw data directly to avoid allocation this->response_data = value.data(); this->response_data_len = value.size(); break; @@ -967,21 +958,15 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation - this->entity_id = value.data(); - this->entity_id_len = value.size(); + this->entity_id = StringRef(reinterpret_cast(value.data()), value.size()); break; } case 2: { - // Use raw data directly to avoid allocation - this->state = value.data(); - this->state_len = value.size(); + this->state = StringRef(reinterpret_cast(value.data()), value.size()); break; } case 3: { - // Use raw data directly to avoid allocation - this->attribute = value.data(); - this->attribute_len = value.size(); + this->attribute = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -993,9 +978,7 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { - // Use raw data directly to avoid allocation - this->timezone = value.data(); - this->timezone_len = value.size(); + this->timezone = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -1060,9 +1043,10 @@ bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) } bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: - this->string_ = value.as_string(); + case 4: { + this->string_ = StringRef(reinterpret_cast(value.data()), value.size()); break; + } case 9: this->string_array.push_back(value.as_string()); break; @@ -1153,7 +1137,7 @@ void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_length(1, this->error_message_ref_.size()); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - size.add_length(4, this->response_data_len); + size.add_length(1, this->response_data_len); #endif } #endif @@ -1408,15 +1392,11 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 17: { - // Use raw data directly to avoid allocation - this->custom_fan_mode = value.data(); - this->custom_fan_mode_len = value.size(); + this->custom_fan_mode = StringRef(reinterpret_cast(value.data()), value.size()); break; } case 21: { - // Use raw data directly to avoid allocation - this->custom_preset = value.data(); - this->custom_preset_len = value.size(); + this->custom_preset = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -1702,9 +1682,7 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { - // Use raw data directly to avoid allocation - this->state = value.data(); - this->state_len = value.size(); + this->state = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -1808,9 +1786,10 @@ bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 5: - this->tone = value.as_string(); + case 5: { + this->tone = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -1899,9 +1878,10 @@ bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: - this->code = value.as_string(); + case 4: { + this->code = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2069,9 +2049,10 @@ bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt val } bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 7: - this->media_url = value.as_string(); + case 7: { + this->media_url = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2279,7 +2260,6 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 4: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -2318,7 +2298,6 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 3: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -2502,12 +2481,14 @@ bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) } bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->name = value.as_string(); + case 1: { + this->name = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->value = value.as_string(); + } + case 2: { + this->value = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2583,12 +2564,14 @@ bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVar } bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: - this->timer_id = value.as_string(); + case 2: { + this->timer_id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 3: - this->name = value.as_string(); + } + case 3: { + this->name = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2606,15 +2589,18 @@ bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt } bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->media_id = value.as_string(); + case 1: { + this->media_id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->text = value.as_string(); + } + case 2: { + this->text = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 3: - this->preannounce_media_id = value.as_string(); + } + case 3: { + this->preannounce_media_id = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2650,24 +2636,29 @@ bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarIn } bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->id = value.as_string(); + case 1: { + this->id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->wake_word = value.as_string(); + } + case 2: { + this->wake_word = StringRef(reinterpret_cast(value.data()), value.size()); break; + } case 3: this->trained_languages.push_back(value.as_string()); break; - case 4: - this->model_type = value.as_string(); + case 4: { + this->model_type = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 6: - this->model_hash = value.as_string(); + } + case 6: { + this->model_hash = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 7: - this->url = value.as_string(); + } + case 7: { + this->url = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2777,9 +2768,10 @@ bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarI } bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: - this->code = value.as_string(); + case 3: { + this->code = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2861,9 +2853,10 @@ bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: - this->state = value.as_string(); + case 2: { + this->state = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -3331,7 +3324,6 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -3356,7 +3348,6 @@ bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -3372,7 +3363,7 @@ void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { } void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->type)); - size.add_length(2, this->data_len); + size.add_length(1, this->data_len); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2111c2a895..9d7a1eb9cb 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -357,12 +357,11 @@ class CommandProtoMessage : public ProtoDecodableMessage { class HelloRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; - static constexpr uint8_t ESTIMATED_SIZE = 27; + static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "hello_request"; } #endif - const uint8_t *client_info{nullptr}; - uint16_t client_info_len{0}; + StringRef client_info{}; uint32_t api_version_major{0}; uint32_t api_version_minor{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -398,12 +397,11 @@ class HelloResponse final : public ProtoMessage { class AuthenticationRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 3; - static constexpr uint8_t ESTIMATED_SIZE = 19; + static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "authentication_request"; } #endif - const uint8_t *password{nullptr}; - uint16_t password_len{0}; + StringRef password{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -784,7 +782,7 @@ class FanStateResponse final : public StateResponseProtoMessage { class FanCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 31; - static constexpr uint8_t ESTIMATED_SIZE = 48; + static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "fan_command_request"; } #endif @@ -797,8 +795,7 @@ class FanCommandRequest final : public CommandProtoMessage { bool has_speed_level{false}; int32_t speed_level{0}; bool has_preset_mode{false}; - const uint8_t *preset_mode{nullptr}; - uint16_t preset_mode_len{0}; + StringRef preset_mode{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -860,7 +857,7 @@ class LightStateResponse final : public StateResponseProtoMessage { class LightCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 32; - static constexpr uint8_t ESTIMATED_SIZE = 122; + static constexpr uint8_t ESTIMATED_SIZE = 112; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "light_command_request"; } #endif @@ -889,8 +886,7 @@ class LightCommandRequest final : public CommandProtoMessage { bool has_flash_length{false}; uint32_t flash_length{0}; bool has_effect{false}; - const uint8_t *effect{nullptr}; - uint16_t effect_len{0}; + StringRef effect{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1171,7 +1167,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { #endif uint32_t call_id{0}; bool success{false}; - std::string error_message{}; + StringRef error_message{}; #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; @@ -1222,16 +1218,13 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { class HomeAssistantStateResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 40; - static constexpr uint8_t ESTIMATED_SIZE = 57; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "home_assistant_state_response"; } #endif - const uint8_t *entity_id{nullptr}; - uint16_t entity_id_len{0}; - const uint8_t *state{nullptr}; - uint16_t state_len{0}; - const uint8_t *attribute{nullptr}; - uint16_t attribute_len{0}; + StringRef entity_id{}; + StringRef state{}; + StringRef attribute{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1256,13 +1249,12 @@ class GetTimeRequest final : public ProtoMessage { class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 24; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; - const uint8_t *timezone{nullptr}; - uint16_t timezone_len{0}; + StringRef timezone{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1310,7 +1302,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { bool bool_{false}; int32_t legacy_int{0}; float float_{0.0f}; - std::string string_{}; + StringRef string_{}; int32_t int_{0}; FixedVector bool_array{}; FixedVector int_array{}; @@ -1499,7 +1491,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { class ClimateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 48; - static constexpr uint8_t ESTIMATED_SIZE = 104; + static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_command_request"; } #endif @@ -1516,13 +1508,11 @@ class ClimateCommandRequest final : public CommandProtoMessage { bool has_swing_mode{false}; enums::ClimateSwingMode swing_mode{}; bool has_custom_fan_mode{false}; - const uint8_t *custom_fan_mode{nullptr}; - uint16_t custom_fan_mode_len{0}; + StringRef custom_fan_mode{}; bool has_preset{false}; enums::ClimatePreset preset{}; bool has_custom_preset{false}; - const uint8_t *custom_preset{nullptr}; - uint16_t custom_preset_len{0}; + StringRef custom_preset{}; bool has_target_humidity{false}; float target_humidity{0.0f}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1695,12 +1685,11 @@ class SelectStateResponse final : public StateResponseProtoMessage { class SelectCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 54; - static constexpr uint8_t ESTIMATED_SIZE = 28; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_command_request"; } #endif - const uint8_t *state{nullptr}; - uint16_t state_len{0}; + StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1756,7 +1745,7 @@ class SirenCommandRequest final : public CommandProtoMessage { bool has_state{false}; bool state{false}; bool has_tone{false}; - std::string tone{}; + StringRef tone{}; bool has_duration{false}; uint32_t duration{0}; bool has_volume{false}; @@ -1817,7 +1806,7 @@ class LockCommandRequest final : public CommandProtoMessage { #endif enums::LockCommand command{}; bool has_code{false}; - std::string code{}; + StringRef code{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1927,7 +1916,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { bool has_volume{false}; float volume{0.0f}; bool has_media_url{false}; - std::string media_url{}; + StringRef media_url{}; bool has_announcement{false}; bool announcement{false}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2503,8 +2492,8 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { }; class VoiceAssistantEventData final : public ProtoDecodableMessage { public: - std::string name{}; - std::string value{}; + StringRef name{}; + StringRef value{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2562,8 +2551,8 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { const char *message_name() const override { return "voice_assistant_timer_event_response"; } #endif enums::VoiceAssistantTimerEvent event_type{}; - std::string timer_id{}; - std::string name{}; + StringRef timer_id{}; + StringRef name{}; uint32_t total_seconds{0}; uint32_t seconds_left{0}; bool is_active{false}; @@ -2582,9 +2571,9 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_announce_request"; } #endif - std::string media_id{}; - std::string text{}; - std::string preannounce_media_id{}; + StringRef media_id{}; + StringRef text{}; + StringRef preannounce_media_id{}; bool start_conversation{false}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2627,13 +2616,13 @@ class VoiceAssistantWakeWord final : public ProtoMessage { }; class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { public: - std::string id{}; - std::string wake_word{}; + StringRef id{}; + StringRef wake_word{}; std::vector trained_languages{}; - std::string model_type{}; + StringRef model_type{}; uint32_t model_size{0}; - std::string model_hash{}; - std::string url{}; + StringRef model_hash{}; + StringRef url{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2734,7 +2723,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { const char *message_name() const override { return "alarm_control_panel_command_request"; } #endif enums::AlarmControlPanelStateCommand command{}; - std::string code{}; + StringRef code{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2791,7 +2780,7 @@ class TextCommandRequest final : public CommandProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_command_request"; } #endif - std::string state{}; + StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 9faf39e29e..567f10fcc0 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -736,7 +736,7 @@ template<> const char *proto_enum_to_string(enums: void HelloRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HelloRequest"); out.append(" client_info: "); - out.append(format_hex_pretty(this->client_info, this->client_info_len)); + out.append("'").append(this->client_info.c_str(), this->client_info.size()).append("'"); out.append("\n"); dump_field(out, "api_version_major", this->api_version_major); dump_field(out, "api_version_minor", this->api_version_minor); @@ -752,7 +752,7 @@ void HelloResponse::dump_to(std::string &out) const { void AuthenticationRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "AuthenticationRequest"); out.append(" password: "); - out.append(format_hex_pretty(this->password, this->password_len)); + out.append("'").append(this->password.c_str(), this->password.size()).append("'"); out.append("\n"); } void AuthenticationResponse::dump_to(std::string &out) const { @@ -965,7 +965,7 @@ void FanCommandRequest::dump_to(std::string &out) const { dump_field(out, "speed_level", this->speed_level); dump_field(out, "has_preset_mode", this->has_preset_mode); out.append(" preset_mode: "); - out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len)); + out.append("'").append(this->preset_mode.c_str(), this->preset_mode.size()).append("'"); out.append("\n"); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1043,7 +1043,7 @@ void LightCommandRequest::dump_to(std::string &out) const { dump_field(out, "flash_length", this->flash_length); dump_field(out, "has_effect", this->has_effect); out.append(" effect: "); - out.append(format_hex_pretty(this->effect, this->effect_len)); + out.append("'").append(this->effect.c_str(), this->effect.size()).append("'"); out.append("\n"); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1205,7 +1205,9 @@ void HomeassistantActionResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeassistantActionResponse"); dump_field(out, "call_id", this->call_id); dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + out.append(" error_message: "); + out.append("'").append(this->error_message.c_str(), this->error_message.size()).append("'"); + out.append("\n"); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON out.append(" response_data: "); out.append(format_hex_pretty(this->response_data, this->response_data_len)); @@ -1226,13 +1228,13 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { void HomeAssistantStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeAssistantStateResponse"); out.append(" entity_id: "); - out.append(format_hex_pretty(this->entity_id, this->entity_id_len)); + out.append("'").append(this->entity_id.c_str(), this->entity_id.size()).append("'"); out.append("\n"); out.append(" state: "); - out.append(format_hex_pretty(this->state, this->state_len)); + out.append("'").append(this->state.c_str(), this->state.size()).append("'"); out.append("\n"); out.append(" attribute: "); - out.append(format_hex_pretty(this->attribute, this->attribute_len)); + out.append("'").append(this->attribute.c_str(), this->attribute.size()).append("'"); out.append("\n"); } #endif @@ -1241,7 +1243,7 @@ void GetTimeResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "GetTimeResponse"); dump_field(out, "epoch_seconds", this->epoch_seconds); out.append(" timezone: "); - out.append(format_hex_pretty(this->timezone, this->timezone_len)); + out.append("'").append(this->timezone.c_str(), this->timezone.size()).append("'"); out.append("\n"); } #ifdef USE_API_USER_DEFINED_ACTIONS @@ -1266,7 +1268,9 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { dump_field(out, "bool_", this->bool_); dump_field(out, "legacy_int", this->legacy_int); dump_field(out, "float_", this->float_); - dump_field(out, "string_", this->string_); + out.append(" string_: "); + out.append("'").append(this->string_.c_str(), this->string_.size()).append("'"); + out.append("\n"); dump_field(out, "int_", this->int_); for (const auto it : this->bool_array) { dump_field(out, "bool_array", static_cast(it), 4); @@ -1424,13 +1428,13 @@ void ClimateCommandRequest::dump_to(std::string &out) const { dump_field(out, "swing_mode", static_cast(this->swing_mode)); dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); out.append(" custom_fan_mode: "); - out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len)); + out.append("'").append(this->custom_fan_mode.c_str(), this->custom_fan_mode.size()).append("'"); out.append("\n"); dump_field(out, "has_preset", this->has_preset); dump_field(out, "preset", static_cast(this->preset)); dump_field(out, "has_custom_preset", this->has_custom_preset); out.append(" custom_preset: "); - out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len)); + out.append("'").append(this->custom_preset.c_str(), this->custom_preset.size()).append("'"); out.append("\n"); dump_field(out, "has_target_humidity", this->has_target_humidity); dump_field(out, "target_humidity", this->target_humidity); @@ -1558,7 +1562,7 @@ void SelectCommandRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "SelectCommandRequest"); dump_field(out, "key", this->key); out.append(" state: "); - out.append(format_hex_pretty(this->state, this->state_len)); + out.append("'").append(this->state.c_str(), this->state.size()).append("'"); out.append("\n"); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1599,7 +1603,9 @@ void SirenCommandRequest::dump_to(std::string &out) const { dump_field(out, "has_state", this->has_state); dump_field(out, "state", this->state); dump_field(out, "has_tone", this->has_tone); - dump_field(out, "tone", this->tone); + out.append(" tone: "); + out.append("'").append(this->tone.c_str(), this->tone.size()).append("'"); + out.append("\n"); dump_field(out, "has_duration", this->has_duration); dump_field(out, "duration", this->duration); dump_field(out, "has_volume", this->has_volume); @@ -1641,7 +1647,9 @@ void LockCommandRequest::dump_to(std::string &out) const { dump_field(out, "key", this->key); dump_field(out, "command", static_cast(this->command)); dump_field(out, "has_code", this->has_code); - dump_field(out, "code", this->code); + out.append(" code: "); + out.append("'").append(this->code.c_str(), this->code.size()).append("'"); + out.append("\n"); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -1719,7 +1727,9 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { dump_field(out, "has_volume", this->has_volume); dump_field(out, "volume", this->volume); dump_field(out, "has_media_url", this->has_media_url); - dump_field(out, "media_url", this->media_url); + out.append(" media_url: "); + out.append("'").append(this->media_url.c_str(), this->media_url.size()).append("'"); + out.append("\n"); dump_field(out, "has_announcement", this->has_announcement); dump_field(out, "announcement", this->announcement); #ifdef USE_DEVICES @@ -1949,8 +1959,12 @@ void VoiceAssistantResponse::dump_to(std::string &out) const { } void VoiceAssistantEventData::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantEventData"); - dump_field(out, "name", this->name); - dump_field(out, "value", this->value); + out.append(" name: "); + out.append("'").append(this->name.c_str(), this->name.size()).append("'"); + out.append("\n"); + out.append(" value: "); + out.append("'").append(this->value.c_str(), this->value.size()).append("'"); + out.append("\n"); } void VoiceAssistantEventResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); @@ -1975,17 +1989,27 @@ void VoiceAssistantAudio::dump_to(std::string &out) const { void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); dump_field(out, "event_type", static_cast(this->event_type)); - dump_field(out, "timer_id", this->timer_id); - dump_field(out, "name", this->name); + out.append(" timer_id: "); + out.append("'").append(this->timer_id.c_str(), this->timer_id.size()).append("'"); + out.append("\n"); + out.append(" name: "); + out.append("'").append(this->name.c_str(), this->name.size()).append("'"); + out.append("\n"); dump_field(out, "total_seconds", this->total_seconds); dump_field(out, "seconds_left", this->seconds_left); dump_field(out, "is_active", this->is_active); } void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); - dump_field(out, "media_id", this->media_id); - dump_field(out, "text", this->text); - dump_field(out, "preannounce_media_id", this->preannounce_media_id); + out.append(" media_id: "); + out.append("'").append(this->media_id.c_str(), this->media_id.size()).append("'"); + out.append("\n"); + out.append(" text: "); + out.append("'").append(this->text.c_str(), this->text.size()).append("'"); + out.append("\n"); + out.append(" preannounce_media_id: "); + out.append("'").append(this->preannounce_media_id.c_str(), this->preannounce_media_id.size()).append("'"); + out.append("\n"); dump_field(out, "start_conversation", this->start_conversation); } void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { dump_field(out, "success", this->success); } @@ -1999,15 +2023,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { } void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + out.append(" id: "); + out.append("'").append(this->id.c_str(), this->id.size()).append("'"); + out.append("\n"); + out.append(" wake_word: "); + out.append("'").append(this->wake_word.c_str(), this->wake_word.size()).append("'"); + out.append("\n"); for (const auto &it : this->trained_languages) { dump_field(out, "trained_languages", it, 4); } - dump_field(out, "model_type", this->model_type); + out.append(" model_type: "); + out.append("'").append(this->model_type.c_str(), this->model_type.size()).append("'"); + out.append("\n"); dump_field(out, "model_size", this->model_size); - dump_field(out, "model_hash", this->model_hash); - dump_field(out, "url", this->url); + out.append(" model_hash: "); + out.append("'").append(this->model_hash.c_str(), this->model_hash.size()).append("'"); + out.append("\n"); + out.append(" url: "); + out.append("'").append(this->url.c_str(), this->url.size()).append("'"); + out.append("\n"); } void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); @@ -2066,7 +2100,9 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); dump_field(out, "key", this->key); dump_field(out, "command", static_cast(this->command)); - dump_field(out, "code", this->code); + out.append(" code: "); + out.append("'").append(this->code.c_str(), this->code.size()).append("'"); + out.append("\n"); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -2103,7 +2139,9 @@ void TextStateResponse::dump_to(std::string &out) const { void TextCommandRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "TextCommandRequest"); dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + out.append(" state: "); + out.append("'").append(this->state.c_str(), this->state.size()).append("'"); + out.append("\n"); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index ccda894509..f9f613ae76 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -7,8 +7,12 @@ #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" +// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars) +static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64; + namespace esphome::ble_client { // placeholder class for static TAG . @@ -151,7 +155,10 @@ template class BLEClientWriteAction : public Action, publ esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); return false; } - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)]; + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); +#endif esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py index c7dca437d7..45a9e54c1e 100644 --- a/esphome/components/bme68x_bsec2/sensor.py +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -50,6 +50,7 @@ TYPES = [ CONFIG_SCHEMA = cv.Schema( { + cv.GenerateID(): cv.declare_id(cg.Component), cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py new file mode 100644 index 0000000000..0e84278afa --- /dev/null +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MAC_ADDRESS + +CODEOWNERS = ["@nagyrobi"] +DEPENDENCIES = ["esp32_ble_tracker"] + +BLE_DEVICE_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA + +bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer") +BTHomeMiThermometer = bthome_mithermometer_ns.class_( + "BTHomeMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + + +def bthome_mithermometer_base_schema(extra_schema=None): + if extra_schema is None: + extra_schema = {} + return ( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + } + ) + .extend(BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) + .extend(extra_schema) + ) + + +async def setup_bthome_mithermometer(var, config): + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp new file mode 100644 index 0000000000..b8da51a783 --- /dev/null +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -0,0 +1,298 @@ +#include "bthome_ble.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace bthome_mithermometer { + +static const char *const TAG = "bthome_mithermometer"; + +static std::string format_mac_address(uint64_t address) { + std::array mac{}; + for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) { + mac[i] = (address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF; + } + + char buffer[MAC_ADDRESS_SIZE * 3]; + format_mac_addr_upper(mac.data(), buffer); + return buffer; +} + +static bool get_bthome_value_length(uint8_t obj_type, size_t &value_length) { + switch (obj_type) { + case 0x00: // packet id + case 0x01: // battery + case 0x09: // count (uint8) + case 0x0F: // generic boolean + case 0x10: // power (bool) + case 0x11: // opening + case 0x15: // battery low + case 0x16: // battery charging + case 0x17: // carbon monoxide + case 0x18: // cold + case 0x19: // connectivity + case 0x1A: // door + case 0x1B: // garage door + case 0x1C: // gas + case 0x1D: // heat + case 0x1E: // light + case 0x1F: // lock + case 0x20: // moisture + case 0x21: // motion + case 0x22: // moving + case 0x23: // occupancy + case 0x24: // plug + case 0x25: // presence + case 0x26: // problem + case 0x27: // running + case 0x28: // safety + case 0x29: // smoke + case 0x2A: // sound + case 0x2B: // tamper + case 0x2C: // vibration + case 0x2D: // water leak + case 0x2E: // humidity (uint8) + case 0x2F: // moisture (uint8) + case 0x46: // UV index + case 0x57: // temperature (sint8) + case 0x58: // temperature (0.35C step) + case 0x59: // count (sint8) + case 0x60: // channel + value_length = 1; + return true; + case 0x02: // temperature (0.01C) + case 0x03: // humidity + case 0x06: // mass (kg) + case 0x07: // mass (lb) + case 0x08: // dewpoint + case 0x0C: // voltage (mV) + case 0x0D: // pm2.5 + case 0x0E: // pm10 + case 0x12: // CO2 + case 0x13: // TVOC + case 0x14: // moisture + case 0x3D: // count (uint16) + case 0x3F: // rotation + case 0x40: // distance (mm) + case 0x41: // distance (m) + case 0x43: // current (A) + case 0x44: // speed + case 0x45: // temperature (0.1C) + case 0x47: // volume (L) + case 0x48: // volume (mL) + case 0x49: // volume flow rate + case 0x4A: // voltage (0.1V) + case 0x51: // acceleration + case 0x52: // gyroscope + case 0x56: // conductivity + case 0x5A: // count (sint16) + case 0x5D: // current (sint16) + case 0x5E: // direction + case 0x5F: // precipitation + case 0x61: // rotational speed + case 0xF0: // button event + value_length = 2; + return true; + case 0x04: // pressure + case 0x05: // illuminance + case 0x0A: // energy + case 0x0B: // power + case 0x42: // duration + case 0x4B: // gas (uint24) + case 0xF2: // firmware version (uint24) + value_length = 3; + return true; + case 0x3E: // count (uint32) + case 0x4C: // gas (uint32) + case 0x4D: // energy (uint32) + case 0x4E: // volume (uint32) + case 0x4F: // water (uint32) + case 0x50: // timestamp + case 0x55: // volume storage + case 0x5B: // count (sint32) + case 0x5C: // power (sint32) + case 0x62: // speed (sint32) + case 0x63: // acceleration (sint32) + case 0xF1: // firmware version (uint32) + value_length = 4; + return true; + default: + return false; + } +} + +void BTHomeMiThermometer::dump_config() { + ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); + ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(this->address_).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + LOG_SENSOR(" ", "Signal Strength", this->signal_strength_); +} + +bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + bool matched = false; + for (auto &service_data : device.get_service_datas()) { + if (this->handle_service_data_(service_data, device)) { + matched = true; + } + } + if (matched && this->signal_strength_ != nullptr) { + this->signal_strength_->publish_state(device.get_rssi()); + } + return matched; +} + +bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, + const esp32_ble_tracker::ESPBTDevice &device) { + if (!service_data.uuid.contains(0xD2, 0xFC)) { + return false; + } + + const auto &data = service_data.data; + if (data.size() < 2) { + ESP_LOGVV(TAG, "BTHome data too short: %zu", data.size()); + return false; + } + + const uint8_t adv_info = data[0]; + const bool is_encrypted = adv_info & 0x01; + const bool mac_included = adv_info & 0x02; + const bool is_trigger_based = adv_info & 0x04; + const uint8_t version = (adv_info >> 5) & 0x07; + + if (version != 0x02) { + ESP_LOGVV(TAG, "Unsupported BTHome version %u", version); + return false; + } + + if (is_encrypted) { + ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str().c_str()); + return false; + } + + size_t payload_index = 1; + uint64_t source_address = device.address_uint64(); + + if (mac_included) { + if (data.size() < 7) { + ESP_LOGVV(TAG, "BTHome payload missing MAC address"); + return false; + } + source_address = 0; + for (int i = 5; i >= 0; i--) { + source_address = (source_address << 8) | data[1 + i]; + } + payload_index = 7; + } + + if (source_address != this->address_) { + ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(source_address).c_str()); + return false; + } + + if (payload_index >= data.size()) { + ESP_LOGVV(TAG, "BTHome payload empty after header"); + return false; + } + + bool reported = false; + size_t offset = payload_index; + uint8_t last_type = 0; + + while (offset < data.size()) { + const uint8_t obj_type = data[offset++]; + size_t value_length = 0; + bool has_length_byte = obj_type == 0x53; // text objects include explicit length + + if (has_length_byte) { + if (offset >= data.size()) { + break; + } + value_length = data[offset++]; + } else { + if (!get_bthome_value_length(obj_type, value_length)) { + ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type); + break; + } + } + + if (value_length == 0) { + break; + } + + if (offset + value_length > data.size()) { + ESP_LOGVV(TAG, "BTHome object length exceeds payload"); + break; + } + + const uint8_t *value = &data[offset]; + offset += value_length; + + if (obj_type < last_type) { + ESP_LOGVV(TAG, "BTHome objects not in ascending order"); + } + last_type = obj_type; + + switch (obj_type) { + case 0x00: { // packet id + const uint8_t packet_id = value[0]; + if (this->last_packet_id_.has_value() && *this->last_packet_id_ == packet_id) { + return reported; + } + this->last_packet_id_ = packet_id; + break; + } + case 0x01: { // battery percentage + if (this->battery_level_ != nullptr) { + this->battery_level_->publish_state(value[0]); + reported = true; + } + break; + } + case 0x0C: { // battery voltage (mV) + if (this->battery_voltage_ != nullptr) { + const uint16_t raw = encode_uint16(value[1], value[0]); + this->battery_voltage_->publish_state(raw * 0.001f); + reported = true; + } + break; + } + case 0x02: { // temperature + if (this->temperature_ != nullptr) { + const int16_t raw = encode_uint16(value[1], value[0]); + this->temperature_->publish_state(raw * 0.01f); + reported = true; + } + break; + } + case 0x03: { // humidity + if (this->humidity_ != nullptr) { + const uint16_t raw = encode_uint16(value[1], value[0]); + this->humidity_->publish_state(raw * 0.01f); + reported = true; + } + break; + } + default: + break; + } + } + + if (reported) { + ESP_LOGD(TAG, "BTHome data%sfrom %s", is_trigger_based ? " (triggered) " : " ", device.address_str().c_str()); + } + + return reported; +} + +} // namespace bthome_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/bthome_mithermometer/bthome_ble.h b/esphome/components/bthome_mithermometer/bthome_ble.h new file mode 100644 index 0000000000..3d2380b48d --- /dev/null +++ b/esphome/components/bthome_mithermometer/bthome_ble.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace bthome_mithermometer { + +class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { + public: + void set_address(uint64_t address) { this->address_ = address; } + + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { this->battery_voltage_ = battery_voltage; } + void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; } + + void dump_config() override; + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + protected: + bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, + const esp32_ble_tracker::ESPBTDevice &device); + + uint64_t address_{0}; + optional last_packet_id_{}; + + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; +}; + +} // namespace bthome_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/bthome_mithermometer/sensor.py b/esphome/components/bthome_mithermometer/sensor.py new file mode 100644 index 0000000000..9b50866db0 --- /dev/null +++ b/esphome/components/bthome_mithermometer/sensor.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_HUMIDITY, + CONF_ID, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, + UNIT_PERCENT, + UNIT_VOLT, +) + +from . import bthome_mithermometer_base_schema, setup_bthome_mithermometer + +CODEOWNERS = ["@nagyrobi"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +CONFIG_SCHEMA = bthome_mithermometer_base_schema( + { + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:battery-plus", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await setup_bthome_mithermometer(var, config) + + if temp_sens := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_sens) + cg.add(var.set_temperature(sens)) + if humi_sens := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humi_sens) + cg.add(var.set_humidity(sens)) + if batl_sens := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(batl_sens) + cg.add(var.set_battery_level(sens)) + if batv_sens := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(batv_sens) + cg.add(var.set_battery_voltage(sens)) + if sgnl_sens := config.get(CONF_SIGNAL_STRENGTH): + sens = await sensor.new_sensor(sgnl_sens) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a3969e081e..a1b684abbf 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -51,7 +51,7 @@ void DallasTemperatureSensor::update() { } float tempc = this->get_temp_c_(); - ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc); + ESP_LOGD(TAG, "'%s': Got Temperature=%f°C", this->get_name().c_str(), tempc); this->publish_state(tempc); }); } diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index c6eaf4e728..602e31db14 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -7,6 +7,9 @@ namespace ee895 { static const char *const TAG = "ee895"; +// Serial number is 16 bytes +static constexpr size_t EE895_SERIAL_NUMBER_SIZE = 16; + static const uint16_t CRC16_ONEWIRE_START = 0xFFFF; static const uint8_t FUNCTION_CODE_READ = 0x03; static const uint16_t SERIAL_NUMBER = 0x0000; @@ -26,7 +29,10 @@ void EE895Component::setup() { this->mark_failed(); return; } - ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char serial_hex[format_hex_size(EE895_SERIAL_NUMBER_SIZE)]; +#endif + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, serial_number + 2, EE895_SERIAL_NUMBER_SIZE)); } void EE895Component::dump_config() { diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 6852416cac..b587b07e8f 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -76,6 +76,12 @@ class EPaperBase : public Display, return 0; } void fill(Color color) override { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + auto pixel_color = color_to_bit(color) ? 0xFF : 0x00; // We store 8 pixels per byte diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index d0e68595d0..be243145fc 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -97,6 +97,12 @@ void EPaperSpectraE6::deep_sleep() { } void EPaperSpectraE6::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + auto pixel_color = color_to_hex(color); // We store 2 pixels per byte diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index dc442cfbd2..d8397a87cc 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -357,11 +357,12 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 2), - "latest": cv.Version(3, 3, 4), - "dev": cv.Version(3, 3, 4), + "recommended": cv.Version(3, 3, 5), + "latest": cv.Version(3, 3, 5), + "dev": cv.Version(3, 3, 5), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), @@ -374,15 +375,33 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { cv.Version(3, 1, 1): cv.Version(53, 3, 11), cv.Version(3, 1, 0): cv.Version(53, 3, 10), } +# Maps Arduino framework versions to a compatible ESP-IDF version +# These versions correspond to pioarduino/esp-idf releases +# See: https://github.com/pioarduino/esp-idf/releases +ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 5): cv.Version(5, 5, 2), + cv.Version(3, 3, 4): cv.Version(5, 5, 1), + cv.Version(3, 3, 3): cv.Version(5, 5, 1), + cv.Version(3, 3, 2): cv.Version(5, 5, 1), + cv.Version(3, 3, 1): cv.Version(5, 5, 1), + cv.Version(3, 3, 0): cv.Version(5, 5, 0), + cv.Version(3, 2, 1): cv.Version(5, 4, 2), + cv.Version(3, 2, 0): cv.Version(5, 4, 2), + cv.Version(3, 1, 3): cv.Version(5, 3, 2), + cv.Version(3, 1, 2): cv.Version(5, 3, 2), + cv.Version(3, 1, 1): cv.Version(5, 3, 1), + cv.Version(3, 1, 0): cv.Version(5, 3, 0), +} # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 1), - "latest": cv.Version(5, 5, 1), - "dev": cv.Version(5, 5, 1), + "recommended": cv.Version(5, 5, 2), + "latest": cv.Version(5, 5, 2), + "dev": cv.Version(5, 5, 2), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 2): cv.Version(55, 3, 35), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), @@ -399,9 +418,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 31, "2"), - "latest": cv.Version(55, 3, 31, "2"), - "dev": cv.Version(55, 3, 31, "2"), + "recommended": cv.Version(55, 3, 35), + "latest": cv.Version(55, 3, 35), + "dev": cv.Version(55, 3, 35), } @@ -727,12 +746,14 @@ FRAMEWORK_SCHEMA = cv.Schema( ) +# Remove this class in 2026.7.0 class _FrameworkMigrationWarning: shown = False def _show_framework_migration_message(name: str, variant: str) -> None: - """Show a friendly message about framework migration when defaulting to Arduino.""" + """Show a message about the framework default change and how to switch back to Arduino.""" + # Remove this function in 2026.7.0 if _FrameworkMigrationWarning.shown: return _FrameworkMigrationWarning.shown = True @@ -742,41 +763,27 @@ def _show_framework_migration_message(name: str, variant: str) -> None: message = ( color( AnsiFore.BOLD_CYAN, - f"💡 IMPORTANT: {name} doesn't have a framework specified!", + f"💡 NOTICE: {name} does not have a framework specified.", ) + "\n\n" - + f"Currently, {variant} defaults to the Arduino framework.\n" - + color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n") + + f"Starting with ESPHome 2026.1.0, the default framework for {variant} is ESP-IDF.\n" + + "(We've been warning about this change since ESPHome 2025.8.0)\n" + "\n" - + "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n" - + "\n" - + "Why change? ESP-IDF offers:\n" - + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n") - + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n") + + "Why we made this change:\n" + + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n") + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") - + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") - + color( - AnsiFore.GREEN, - " 🔧 Active development and testing by ESPHome developers\n", - ) + + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n") + + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n") + "\n" - + "Trade-offs:\n" - + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + + "To continue using Arduino, add this to your YAML under 'esp32:':\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: arduino\n") + "\n" - + "What should I do?\n" - + color(AnsiFore.CYAN, " Option 1") - + ": Migrate to ESP-IDF (recommended)\n" - + " Add this to your YAML under 'esp32:':\n" - + color(AnsiFore.WHITE, " framework:\n") - + color(AnsiFore.WHITE, " type: esp-idf\n") + + "To silence this message with ESP-IDF, explicitly set:\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: esp-idf\n") + "\n" - + color(AnsiFore.CYAN, " Option 2") - + ": Keep using Arduino (still supported)\n" - + " Add this to your YAML under 'esp32:':\n" - + color(AnsiFore.WHITE, " framework:\n") - + color(AnsiFore.WHITE, " type: arduino\n") - + "\n" - + "Need help? Check out the migration guide:\n" + + "Migration guide: " + color( AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf/", @@ -791,13 +798,13 @@ def _set_default_framework(config): config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) if CONF_TYPE not in config[CONF_FRAMEWORK]: variant = config[CONF_VARIANT] + config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF + # Show migration message for variants that previously defaulted to Arduino + # Remove this message in 2026.7.0 if variant in ARDUINO_ALLOWED_VARIANTS: - config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO _show_framework_migration_message( config.get(CONF_NAME, "This device"), variant ) - else: - config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF return config @@ -991,6 +998,13 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) + # Add IDF framework source for Arduino builds to ensure it uses the same version as + # the ESP-IDF framework + if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: + cg.add_platformio_option( + "platform_packages", [_format_framework_espidf_version(idf_ver, None)] + ) + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency if get_esp32_variant() == VARIANT_ESP32S2: cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 514d674b55..8a7a9428db 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1488,6 +1488,10 @@ BOARDS = { "name": "Arduino Nano ESP32", "variant": VARIANT_ESP32S3, }, + "arduino_nesso_n1": { + "name": "Arduino Nesso-N1", + "variant": VARIANT_ESP32C6, + }, "atd147_s3": { "name": "ArtronShop ATD1.47-S3", "variant": VARIANT_ESP32S3, @@ -1656,6 +1660,10 @@ BOARDS = { "name": "Espressif ESP32-C6-DevKitM-1", "variant": VARIANT_ESP32C6, }, + "esp32-c61-devkitc1-n8r2": { + "name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)", + "variant": VARIANT_ESP32C61, + }, "esp32-devkitlipo": { "name": "OLIMEX ESP32-DevKit-LiPo", "variant": VARIANT_ESP32, @@ -1673,11 +1681,15 @@ BOARDS = { "variant": VARIANT_ESP32H2, }, "esp32-p4": { - "name": "Espressif ESP32-P4 generic", + "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, }, "esp32-p4-evboard": { - "name": "Espressif ESP32-P4 Function EV Board", + "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", + "variant": VARIANT_ESP32P4, + }, + "esp32-p4_r3": { + "name": "Espressif ESP32-P4 rev.300 generic", "variant": VARIANT_ESP32P4, }, "esp32-pico-devkitm-2": { @@ -2093,7 +2105,7 @@ BOARDS = { "variant": VARIANT_ESP32, }, "m5stack-tab5-p4": { - "name": "M5STACK Tab5 esp32-p4 Board", + "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, }, "m5stack-timer-cam": { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 47da2e3570..63675ec377 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -37,6 +37,9 @@ namespace esphome::esp32_ble_tracker { static const char *const TAG = "esp32_ble_tracker"; +// BLE advertisement max: 31 bytes adv data + 31 bytes scan response +static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62; + ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) const char *client_state_to_string(ClientState state) { @@ -445,6 +448,7 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { uuid.to_str(uuid_buf); ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf); } + char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)]; for (auto &data : this->manufacturer_datas_) { auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data); if (ibeacon.has_value()) { @@ -458,7 +462,8 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { } else { char uuid_buf[esp32_ble::UUID_STR_LEN]; data.uuid.to_str(uuid_buf); - ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str()); + ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, + format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } } for (auto &data : this->service_datas_) { @@ -466,11 +471,11 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { char uuid_buf[esp32_ble::UUID_STR_LEN]; data.uuid.to_str(uuid_buf); ESP_LOGVV(TAG, " UUID: %s", uuid_buf); - ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); + ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } ESP_LOGVV(TAG, " Adv data: %s", - format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str()); + format_hex_pretty_to(hex_buf, scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len)); #endif } diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index e56e85b231..2f637d714d 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -16,7 +16,7 @@ from esphome.const import ( CONF_SAFE_MODE, CONF_VERSION, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import coroutine_with_priority from esphome.coroutine import CoroPriority import esphome.final_validate as fv from esphome.types import ConfigType @@ -28,17 +28,7 @@ CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] -def supports_sha256() -> bool: - """Check if the current platform supports SHA256 for OTA authentication.""" - return bool(CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny) - - -def AUTO_LOAD() -> list[str]: - """Conditionally auto-load sha256 only on platforms that support it.""" - base_components = ["md5", "socket"] - if supports_sha256(): - return base_components + ["sha256"] - return base_components +AUTO_LOAD = ["sha256", "socket"] esphome = cg.esphome_ns.namespace("esphome") @@ -155,11 +145,6 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_PASSWORD): cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_PASSWORD") - # Only include hash algorithms when password is configured - cg.add_define("USE_OTA_MD5") - # Only include SHA256 support on platforms that have it - if supports_sha256(): - cg.add_define("USE_OTA_SHA256") cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) await cg.register_component(var, config) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f9984e1425..16d7089f02 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,13 +1,8 @@ #include "ota_esphome.h" #ifdef USE_OTA #ifdef USE_OTA_PASSWORD -#ifdef USE_OTA_MD5 -#include "esphome/components/md5/md5.h" -#endif -#ifdef USE_OTA_SHA256 #include "esphome/components/sha256/sha256.h" #endif -#endif #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_esp8266.h" @@ -31,15 +26,6 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer -#ifdef USE_OTA_PASSWORD -#ifdef USE_OTA_MD5 -static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2) -#endif -#ifdef USE_OTA_SHA256 -static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2) -#endif -#endif // USE_OTA_PASSWORD - void ESPHomeOTAComponent::setup() { this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { @@ -108,15 +94,7 @@ void ESPHomeOTAComponent::loop() { } static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; -#ifdef USE_OTA_SHA256 static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; -#endif - -// Temporary flag to allow MD5 downgrade for ~3 versions (until 2026.1.0) -// This allows users to downgrade via OTA if they encounter issues after updating. -// Without this, users would need to do a serial flash to downgrade. -// TODO: Remove this flag and all associated code in 2026.1.0 -#define ALLOW_OTA_DOWNGRADE_MD5 void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. @@ -547,26 +525,8 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } bool ESPHomeOTAComponent::select_auth_type_() { -#ifdef USE_OTA_SHA256 bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; -#ifdef ALLOW_OTA_DOWNGRADE_MD5 - // Allow fallback to MD5 if client doesn't support SHA256 - if (client_supports_sha256) { - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - return true; - } -#ifdef USE_OTA_MD5 - this->log_auth_warning_(LOG_STR("Using deprecated MD5")); - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; - return true; -#else - this->log_auth_warning_(LOG_STR("SHA256 required")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; -#endif // USE_OTA_MD5 - -#else // !ALLOW_OTA_DOWNGRADE_MD5 // Require SHA256 if (!client_supports_sha256) { this->log_auth_warning_(LOG_STR("SHA256 required")); @@ -575,20 +535,6 @@ bool ESPHomeOTAComponent::select_auth_type_() { } this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; return true; -#endif // ALLOW_OTA_DOWNGRADE_MD5 - -#else // !USE_OTA_SHA256 -#ifdef USE_OTA_MD5 - // Only MD5 available - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; - return true; -#else - // No auth methods available - this->log_auth_warning_(LOG_STR("No auth methods available")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; -#endif // USE_OTA_MD5 -#endif // USE_OTA_SHA256 } bool ESPHomeOTAComponent::handle_auth_send_() { @@ -612,31 +558,12 @@ bool ESPHomeOTAComponent::handle_auth_send_() { // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - // Declare both hash objects in same stack frame, use pointer to select. - // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 - // hardware SHA acceleration - the object must exist in this stack frame for all operations. - // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. -#ifdef USE_OTA_SHA256 - sha256::SHA256 sha_hasher; -#endif -#ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; -#endif - HashBase *hasher = nullptr; + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // (no passing to other functions). All hash operations must happen in this function. + sha256::SHA256 hasher; -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hasher = &sha_hasher; - } -#endif -#ifdef USE_OTA_MD5 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - hasher = &md5_hasher; - } -#endif - - const size_t hex_size = hasher->get_size() * 2; - const size_t nonce_len = hasher->get_size() / 4; + const size_t hex_size = hasher.get_size() * 2; + const size_t nonce_len = hasher.get_size() / 4; const size_t auth_buf_size = 1 + 3 * hex_size; this->auth_buf_ = std::make_unique(auth_buf_size); this->auth_buf_pos_ = 0; @@ -648,22 +575,17 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); + hasher.init(); + hasher.add(buf, nonce_len); + hasher.calculate(); this->auth_buf_[0] = this->auth_type_; - hasher->get_hex(buf); + hasher.get_hex(buf); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); -#endif + ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf); } // Try to write auth_type + nonce - size_t hex_size = this->get_auth_hex_size_(); + constexpr size_t hex_size = SHA256_HEX_SIZE; const size_t to_write = 1 + hex_size; size_t remaining = to_write - this->auth_buf_pos_; @@ -685,7 +607,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { } bool ESPHomeOTAComponent::handle_auth_read_() { - size_t hex_size = this->get_auth_hex_size_(); + constexpr size_t hex_size = SHA256_HEX_SIZE; const size_t to_read = hex_size * 2; // CNonce + Response // Try to read remaining bytes (CNonce + Response) @@ -710,55 +632,25 @@ bool ESPHomeOTAComponent::handle_auth_read_() { const char *cnonce = nonce + hex_size; const char *response = cnonce + hex_size; - // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions). - // Declare both hash objects in same stack frame, use pointer to select. - // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 - // hardware SHA acceleration - the object must exist in this stack frame for all operations. - // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. -#ifdef USE_OTA_SHA256 - sha256::SHA256 sha_hasher; -#endif -#ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; -#endif - HashBase *hasher = nullptr; + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // (no passing to other functions). All hash operations must happen in this function. + sha256::SHA256 hasher; -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hasher = &sha_hasher; - } -#endif -#ifdef USE_OTA_MD5 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - hasher = &md5_hasher; - } -#endif - - hasher->init(); - hasher->add(this->password_.c_str(), this->password_.length()); - hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) - hasher->calculate(); + hasher.init(); + hasher.add(this->password_.c_str(), this->password_.length()); + hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) + hasher.calculate(); + ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too - // Log CNonce - memcpy(log_buf, cnonce, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); - - // Log computed hash - hasher->get_hex(log_buf); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Result is %s", log_buf); - - // Log received response - memcpy(log_buf, response, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Response is %s", log_buf); + char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator) + hasher.get_hex(computed_hash); + ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash); #endif + ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response); // Compare response - bool matches = hasher->equals_hex(response); + bool matches = hasher.equals_hex(response); if (!matches) { this->log_auth_warning_(LOG_STR("Password mismatch")); @@ -772,21 +664,6 @@ bool ESPHomeOTAComponent::handle_auth_read_() { return true; } -size_t ESPHomeOTAComponent::get_auth_hex_size_() const { -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - return SHA256_HEX_SIZE; - } -#endif -#ifdef USE_OTA_MD5 - return MD5_HEX_SIZE; -#else -#ifndef USE_OTA_SHA256 -#error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled" -#endif -#endif -} - void ESPHomeOTAComponent::cleanup_auth_() { this->auth_buf_ = nullptr; this->auth_buf_pos_ = 0; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 4412a65757..e199b7e406 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -44,10 +44,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void handle_handshake_(); void handle_data_(); #ifdef USE_OTA_PASSWORD + static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2) bool handle_auth_send_(); bool handle_auth_read_(); bool select_auth_type_(); - size_t get_auth_hex_size_() const; void cleanup_auth_(); void log_auth_warning_(const LogString *msg); #endif // USE_OTA_PASSWORD diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index bc05833709..16e2331937 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -6,6 +6,7 @@ #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -299,9 +300,10 @@ void ESPNowComponent::loop() { // Intentionally left as if instead of else in case the peer is added above if (esp_now_is_peer_exist(info.src_addr)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)]; ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(), format_mac_address_pretty(info.des_addr).c_str(), - format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str()); + format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size)); #endif if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { for (auto *handler : this->broadcasted_handlers_) { diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 114000401f..af4f652d8b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -1,5 +1,6 @@ #include "ethernet_component.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -39,6 +40,9 @@ namespace ethernet { static const char *const TAG = "ethernet"; +// PHY register size for hex logging +static constexpr size_t PHY_REG_SIZE = 2; + EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { @@ -773,7 +777,10 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { uint32_t phy_control_2; err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; +#endif + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); /* * Bit 7 is `RMII Reference Clock Select`. Default is `0`. @@ -790,7 +797,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", + format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); } } #endif // USE_ETHERNET_KSZ8081 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index 329fb9113a..35e18c7de5 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ethernet_info { +namespace esphome::ethernet_info { static const char *const TAG = "ethernet_info"; @@ -12,7 +11,6 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP void DNSAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo DNS Address", this); } void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo MAC Address", this); } -} // namespace ethernet_info -} // namespace esphome +} // namespace esphome::ethernet_info #endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 2adc08e31e..b49ddc263d 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ethernet_info { +namespace esphome::ethernet_info { class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { public: @@ -40,21 +39,27 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - auto dns_one = ethernet::global_eth_component->get_dns_address(0); - auto dns_two = ethernet::global_eth_component->get_dns_address(1); + auto dns1 = ethernet::global_eth_component->get_dns_address(0); + auto dns2 = ethernet::global_eth_component->get_dns_address(1); - std::string dns_results = dns_one.str() + " " + dns_two.str(); - - if (dns_results != this->last_results_) { - this->last_results_ = dns_results; - this->publish_state(dns_results); + if (dns1 != this->last_dns1_ || dns2 != this->last_dns2_) { + this->last_dns1_ = dns1; + this->last_dns2_ = dns2; + // IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot + char buf[network::IP_ADDRESS_BUFFER_SIZE * 2]; + dns1.str_to(buf); + size_t len1 = strlen(buf); + buf[len1] = ' '; + dns2.str_to(buf + len1 + 1); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::ETHERNET; } void dump_config() override; protected: - std::string last_results_; + network::IPAddress last_dns1_; + network::IPAddress last_dns2_; }; class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor { @@ -64,7 +69,6 @@ class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor void dump_config() override; }; -} // namespace ethernet_info -} // namespace esphome +} // namespace esphome::ethernet_info #endif // USE_ESP32 diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index ab15a2340d..c0f14c7105 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -8,6 +8,9 @@ namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; +// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) +static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; + void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); this->set_enrolling_(false); @@ -142,7 +145,10 @@ void HlkFm22xComponent::recv_command_() { data.push_back(byte); } - ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); +#endif byte = this->read(); if (byte != checksum) { diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h index b83b9d5421..fb9479e3af 100644 --- a/esphome/components/hmac_md5/hmac_md5.h +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -30,7 +30,7 @@ class HmacMD5 { void get_bytes(uint8_t *output); /// Retrieve the HMAC-MD5 digest as hex characters. - /// The output must be able to hold 32 bytes or more. + /// The output must be able to hold 33 bytes or more (32 hex chars + null terminator). void get_hex(char *output); /// Compare the digest against a provided byte-encoded digest (16 bytes). diff --git a/esphome/components/hmac_sha256/hmac_sha256.h b/esphome/components/hmac_sha256/hmac_sha256.h index fa6b64aa94..85622cac46 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.h +++ b/esphome/components/hmac_sha256/hmac_sha256.h @@ -35,7 +35,7 @@ class HmacSHA256 { void get_bytes(uint8_t *output); /// Retrieve the HMAC-SHA256 digest as hex characters. - /// The output must be able to hold 64 bytes or more. + /// The output must be able to hold 65 bytes or more (64 hex chars + null terminator). void get_hex(char *output); /// Compare the digest against a provided byte-encoded digest (32 bytes). diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp index 7317174831..e29f1a898c 100644 --- a/esphome/components/hub75/hub75.cpp +++ b/esphome/components/hub75/hub75.cpp @@ -111,6 +111,9 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]] return; + if (!this->get_clipping().inside(x, y)) + return; + driver_->set_pixel(x, y, color.r, color.g, color.b); App.feed_wdt(); } diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 1579020c9b..e728830147 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -12,6 +12,9 @@ namespace i2c { static const char *const TAG = "i2c.arduino"; +// Maximum bytes to log in hex format (truncates larger transfers) +static constexpr size_t I2C_MAX_LOG_BYTES = 32; + void ArduinoI2CBus::setup() { recover_(); @@ -107,7 +110,10 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe return ERROR_NOT_INITIALIZED; } - ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)]; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count)); +#endif uint8_t status = 0; if (write_count != 0 || read_count == 0) { diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 486dc0b7d8..191c849aa3 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -15,6 +15,9 @@ namespace i2c { static const char *const TAG = "i2c.idf"; +// Maximum bytes to log in hex format (truncates larger transfers) +static constexpr size_t I2C_MAX_LOG_BYTES = 32; + void IDFI2CBus::setup() { static i2c_port_t next_hp_port = I2C_NUM_0; #if SOC_LP_I2C_SUPPORTED @@ -147,7 +150,10 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s jobs[num_jobs++].write.total_bytes = 1; } else { if (write_count != 0) { - ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)]; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count)); +#endif jobs[num_jobs++].command = I2C_MASTER_CMD_START; jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; jobs[num_jobs].write.ack_check = true; diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 2a3d0edca7..a3eff901d3 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -131,6 +131,13 @@ float ILI9XXXDisplay::get_setup_priority() const { return setup_priority::HARDWA void ILI9XXXDisplay::fill(Color color) { if (!this->check_buffer_()) return; + + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + uint16_t new_color = 0; this->x_low_ = 0; this->y_low_ = 0; diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index f96fb6905e..c921c643fa 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -293,6 +293,13 @@ void Inkplate::fill(Color color) { ESP_LOGV(TAG, "Fill called"); uint32_t start_time = millis(); + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + return; + } + if (this->greyscale_) { uint8_t fill = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; memset(this->buffer_, (fill << 4) | fill, this->get_buffer_length_()); diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index 2ef8cf2649..34d7baf880 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -8,8 +8,9 @@ extern "C" { uint8_t temprature_sens_read(); } #elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) #include "driver/temperature_sensor.h" #endif // USE_ESP32_VARIANT #endif // USE_ESP32 @@ -27,9 +28,9 @@ namespace internal_temperature { static const char *const TAG = "internal_temperature"; #ifdef USE_ESP32 -#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) static temperature_sensor_handle_t tsensNew = NULL; #endif // USE_ESP32_VARIANT #endif // USE_ESP32 @@ -44,8 +45,9 @@ void InternalTemperatureSensor::update() { temperature = (raw - 32) / 1.8f; success = (raw != 128); #elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature); success = (result == ESP_OK); if (!success) { @@ -81,9 +83,9 @@ void InternalTemperatureSensor::update() { void InternalTemperatureSensor::setup() { #ifdef USE_ESP32 -#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew); diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 3365d1f417..7679002e74 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -36,7 +36,7 @@ static const char *get_color_mode_json_str(ColorMode mode) { void LightJSONSchema::dump_json(LightState &state, JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (state.supports_effects()) { - root[ESPHOME_F("effect")] = state.get_effect_name(); + root[ESPHOME_F("effect")] = state.get_effect_name_ref(); root[ESPHOME_F("effect_index")] = state.get_current_effect_index(); root[ESPHOME_F("effect_count")] = state.get_effect_count(); } diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 21530441f8..34ac9c51f7 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -85,11 +85,11 @@ class ArcType(NumberType): lv.arc_set_range(w.obj, min_value, max_value) await w.set_property( - CONF_START_ANGLE, + "bg_start_angle", await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), ) await w.set_property( - CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) + "bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) ) await w.set_property( CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index cae8647398..18cafab684 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -1,10 +1,14 @@ #ifdef USE_ESP32_VARIANT_ESP32P4 #include #include "mipi_dsi.h" +#include "esphome/core/helpers.h" namespace esphome { namespace mipi_dsi { +// Maximum bytes to log for init commands (truncated if larger) +static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64; + static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { auto *sem = static_cast(user_ctx); BaseType_t need_yield = pdFALSE; @@ -121,8 +125,11 @@ void MIPI_DSI::setup() { } } const auto *ptr = vec.data() + index; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)]; +#endif ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args, - format_hex_pretty(ptr, num_args, '.', false).c_str()); + format_hex_pretty_to(hex_buf, ptr, num_args, '.')); err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); if (err != ESP_OK) { this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err); @@ -293,6 +300,13 @@ void MIPI_DSI::draw_pixel_at(int x, int y, Color color) { void MIPI_DSI::fill(Color color) { if (!this->check_buffer_()) return; + + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + switch (this->color_depth_) { case display::COLOR_BITNESS_565: { auto *ptr_16 = reinterpret_cast(this->buffer_); diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index d5d1caf6d2..ef96da8a1c 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP32_VARIANT_ESP32S3 #include "mipi_rgb.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" #include "esp_lcd_panel_rgb.h" @@ -8,6 +9,9 @@ namespace esphome { namespace mipi_rgb { static const uint8_t DELAY_FLAG = 0xFF; + +// Maximum bytes to log for init commands (truncated if larger) +static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64; static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes @@ -91,8 +95,9 @@ void MipiRgbSpi::write_init_sequence_() { delay(120); // NOLINT } const auto *ptr = vec.data() + index; + char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)]; ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args, - format_hex_pretty(ptr, num_args, '.', false).c_str()); + format_hex_pretty_to(hex_buf, ptr, num_args, '.')); index += num_args; this->write_command_(cmd); while (num_args-- != 0) @@ -300,6 +305,13 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) { void MipiRgb::fill(Color color) { if (!this->check_buffer_()) return; + + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + auto *ptr_16 = reinterpret_cast(this->buffer_); uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 1953aef035..a59cb8104b 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -5,11 +5,15 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" +#include "esphome/core/helpers.h" namespace esphome { namespace mipi_spi { constexpr static const char *const TAG = "display.mipi_spi"; + +// Maximum bytes to log for commands (truncated if larger) +static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64; static constexpr uint8_t SW_RESET_CMD = 0x01; static constexpr uint8_t SLEEP_OUT = 0x11; static constexpr uint8_t NORON = 0x13; @@ -241,7 +245,10 @@ class MipiSpi : public display::Display, // Writes a command to the display, with the given bytes. void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { - esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)]; + esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); +#endif if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { this->enable(); this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); @@ -562,6 +569,12 @@ class MipiSpiBuffer : public MipiSpiget_clipping().is_set()) { + display::Display::fill(color); + return; + } + this->x_low_ = 0; this->y_low_ = this->start_line_; this->x_high_ = WIDTH - 1; diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index 10ab4f3b5c..d80b7aeff5 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -1,4 +1,5 @@ #include "mitsubishi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -6,6 +7,9 @@ namespace mitsubishi { static const char *const TAG = "mitsubishi.climate"; +// IR frame size for Mitsubishi climate +static constexpr size_t MITSUBISHI_FRAME_SIZE = 18; + const uint8_t MITSUBISHI_OFF = 0x00; const uint8_t MITSUBISHI_MODE_AUTO = 0x20; @@ -388,7 +392,10 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) { break; } - ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty(state_frame, 18).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MITSUBISHI_FRAME_SIZE)]; +#endif + ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty_to(hex_buf, state_frame, MITSUBISHI_FRAME_SIZE)); this->publish_state(); return true; diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 20271b4bdb..457dff4075 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -8,6 +8,9 @@ namespace modbus { static const char *const TAG = "modbus"; +// Maximum bytes to log for Modbus frames (truncated if larger) +static constexpr size_t MODBUS_MAX_LOG_BYTES = 64; + void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); @@ -255,7 +258,10 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address this->flow_control_pin_->digital_write(false); waiting_for_response = address; last_send_ = millis(); - ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); } // Helper function for lambdas @@ -276,7 +282,10 @@ void Modbus::send_raw(const std::vector &payload) { if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); waiting_for_response = payload[0]; - ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty(payload).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size())); last_send_ = millis(); } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 0d8340f95f..986a9a9fdc 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -13,6 +13,9 @@ static const uint16_t SERVICE_UUID = 0xADA0; static const uint8_t MANUFACTURER_DATA_LENGTH = 23; static const uint16_t MANUFACTURER_ID = 0x000D; +// Maximum bytes to log in very verbose hex output +static constexpr size_t MOPEKA_MAX_LOG_BYTES = 32; + void MopekaStdCheck::dump_config() { ESP_LOGCONFIG(TAG, "Mopeka Std Check"); ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100); @@ -60,7 +63,11 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const auto &manu_data = manu_datas[0]; - ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MOPEKA_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(), + format_hex_pretty_to(hex_buf, manu_data.data.data(), manu_data.data.size())); if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { ESP_LOGE(TAG, "[%s] Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size()); diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index f5b018b127..95d91ff18a 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -117,6 +117,12 @@ void PCD8544::update() { } void PCD8544::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 0871f7acab..118421c47f 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -1,4 +1,5 @@ #include "pn532_spi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" // Based on: @@ -11,6 +12,9 @@ namespace pn532_spi { static const char *const TAG = "pn532_spi"; +// Maximum bytes to log in verbose hex output +static constexpr size_t PN532_MAX_LOG_BYTES = 64; + void PN532Spi::setup() { this->spi_setup(); @@ -32,7 +36,10 @@ bool PN532Spi::write_data(const std::vector &data) { delay(2); // First byte, communication mode: Write data this->write_byte(0x01); - ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); this->write_array(data.data(), data.size()); this->disable(); @@ -55,7 +62,10 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { this->read_array(data.data(), len); this->disable(); data.insert(data.begin(), 0x01); - ESP_LOGV(TAG, "Read data: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Read data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); return true; } @@ -73,7 +83,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { std::vector header(7); this->read_array(header.data(), 7); - ESP_LOGV(TAG, "Header data: %s", format_hex_pretty(header).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size())); if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { // invalid packet @@ -103,7 +116,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { this->read_array(data.data(), len + 1); this->disable(); - ESP_LOGV(TAG, "Response data: %s", format_hex_pretty(data).c_str()); + ESP_LOGV(TAG, "Response data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); uint8_t checksum = header[5] + header[6]; // TFI + Command response code for (int i = 0; i < len - 1; i++) { diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index 8637506bae..06837b94ab 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -60,13 +60,13 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { * Valid values are from -99.5 to 1999.5. Smaller values are displayed as Lo, higher as Hi. * It will printed as it fits in the screen. */ - void print_bignum(float bignum) { this->bignum_ = bignum * 10; } + void print_bignum(float bignum) { this->bignum_ = static_cast(bignum * 10); } /** * Print the small number * * Valid values are from -9 to 99. Smaller values are displayed as Lo, higher as Hi. */ - void print_smallnum(float smallnum) { this->smallnum_ = smallnum; } + void print_smallnum(float smallnum) { this->smallnum_ = static_cast(smallnum); } /** * Print a happy face * @@ -107,8 +107,8 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { bool auto_clear_enabled_{true}; uint32_t disconnect_delay_ms_ = 5000; uint16_t validity_period_ = 300; - uint16_t bignum_ = 0; - uint16_t smallnum_ = 0; + int16_t bignum_ = 0; + int16_t smallnum_ = 0; uint8_t cfg_ = 0; void setcfgbit_(uint8_t bit, bool value); diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp index 24b9a0ce0a..00a4a375eb 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.cpp +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -1,10 +1,14 @@ #if defined(USE_ESP32) && defined(USE_ESP32_VARIANT_ESP32S3) #include "qspi_dbi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { namespace qspi_dbi { +// Maximum bytes to log in verbose hex output +static constexpr size_t QSPI_DBI_MAX_LOG_BYTES = 64; + void QspiDbi::setup() { this->spi_setup(); if (this->enable_pin_ != nullptr) { @@ -174,7 +178,11 @@ void QspiDbi::write_to_display_(int x_start, int y_start, int w, int h, const ui this->disable(); } void QspiDbi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { - ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(QSPI_DBI_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, + format_hex_pretty_to(hex_buf, sizeof(hex_buf), bytes, len)); this->enable(); this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); this->disable(); diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index c815c98419..b9ce1f9151 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -10,6 +10,9 @@ namespace seeed_mr60bha2 { static const char *const TAG = "seeed_mr60bha2"; +// Maximum bytes to log in verbose hex output +static constexpr size_t MR60BHA2_MAX_LOG_BYTES = 64; + // Prints the component's configuration data. dump_config() prints all of the component's configuration // items in an easy-to-read format, including the configuration key-value pairs. void MR60BHA2Component::dump_config() { @@ -110,7 +113,10 @@ bool MR60BHA2Component::validate_message_() { if (at == 7) { if (!validate_checksum(data, 7, header_checksum)) { ESP_LOGE(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", header_checksum); - ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, 8)); return false; } return true; @@ -125,14 +131,22 @@ bool MR60BHA2Component::validate_message_() { if (at == 8 + length) { if (!validate_checksum(data + 8, length, data_checksum)) { ESP_LOGE(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", data_checksum); - ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8 + length).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, 8 + length)); return false; } } const uint8_t *frame_data = data + 8; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf1[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; + char hex_buf2[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; +#endif ESP_LOGV(TAG, "Received Frame: ID: 0x%04x, Type: 0x%04x, Data: [%s] Raw Data: [%s]", frame_id, frame_type, - format_hex_pretty(frame_data, length).c_str(), format_hex_pretty(this->rx_message_).c_str()); + format_hex_pretty_to(hex_buf1, sizeof(hex_buf1), frame_data, length), + format_hex_pretty_to(hex_buf2, sizeof(hex_buf2), this->rx_message_.data(), this->rx_message_.size())); this->process_frame_(frame_id, frame_type, data + 8, length); // Return false to reset rx buffer diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 7f8bd6a43c..b5b5b4d05a 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -10,6 +10,9 @@ namespace seeed_mr60fda2 { static const char *const TAG = "seeed_mr60fda2"; +// Maximum bytes to log in verbose hex output +static constexpr size_t MR60FDA2_MAX_LOG_BYTES = 64; + // Prints the component's configuration data. dump_config() prints all of the component's configuration // items in an easy-to-read format, including the configuration key-value pairs. void MR60FDA2Component::dump_config() { @@ -202,9 +205,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { this->current_frame_locate_++; } else { ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char frame_buf[format_hex_pretty_size(MR60FDA2_MAX_LOG_BYTES)]; + char byte_buf[format_hex_pretty_size(1)]; +#endif ESP_LOGV(TAG, "CURRENT_FRAME: %s %s", - format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(), - format_hex_pretty(&buffer, 1).c_str()); + format_hex_pretty_to(frame_buf, this->current_frame_buf_, this->current_frame_len_), + format_hex_pretty_to(byte_buf, &buffer, 1)); this->current_frame_locate_ = LOCATE_FRAME_HEADER; } break; @@ -228,9 +235,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { this->process_frame_(); } else { ESP_LOGD(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", buffer); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char frame_buf[format_hex_pretty_size(MR60FDA2_MAX_LOG_BYTES)]; + char byte_buf[format_hex_pretty_size(1)]; +#endif ESP_LOGV(TAG, "GET CURRENT_FRAME: %s %s", - format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(), - format_hex_pretty(&buffer, 1).c_str()); + format_hex_pretty_to(frame_buf, this->current_frame_buf_, this->current_frame_len_), + format_hex_pretty_to(byte_buf, &buffer, 1)); this->current_frame_locate_ = LOCATE_FRAME_HEADER; } @@ -328,7 +339,10 @@ void MR60FDA2Component::set_install_height(uint8_t index) { float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); this->write_array(send_data, 13); - ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty(send_data, 13).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(13)]; +#endif + ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty_to(hex_buf, send_data, 13)); } void MR60FDA2Component::set_height_threshold(uint8_t index) { @@ -336,7 +350,10 @@ void MR60FDA2Component::set_height_threshold(uint8_t index) { float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); this->write_array(send_data, 13); - ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty(send_data, 13).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(13)]; +#endif + ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty_to(hex_buf, send_data, 13)); } void MR60FDA2Component::set_sensitivity(uint8_t index) { @@ -346,19 +363,28 @@ void MR60FDA2Component::set_sensitivity(uint8_t index) { send_data[12] = calculate_checksum(send_data + 8, 4); this->write_array(send_data, 13); - ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty(send_data, 13).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(13)]; +#endif + ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty_to(hex_buf, send_data, 13)); } void MR60FDA2Component::get_radar_parameters() { uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x06, 0xF6}; this->write_array(send_data, 8); - ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty(send_data, 8).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(8)]; +#endif + ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty_to(hex_buf, send_data, 8)); } void MR60FDA2Component::factory_reset() { uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x21, 0x10, 0xCF}; this->write_array(send_data, 8); - ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty(send_data, 8).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(8)]; +#endif + ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty_to(hex_buf, send_data, 8)); this->get_radar_parameters(); } diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index fc47a6dc5e..a9ada432e4 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -70,7 +70,7 @@ void SN74HC595GPIOComponent::write_gpio() { void SN74HC595SPIComponent::write_gpio() { for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { this->enable(); - this->transfer_byte(output_byte); + this->write_byte(output_byte); this->disable(); } SN74HC595Component::write_gpio(); diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 045cdd09d3..e890567abf 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -49,21 +49,60 @@ SPIDevice = spi_ns.class_("SPIDevice") SPIDataRate = spi_ns.enum("SPIDataRate") SPIMode = spi_ns.enum("SPIMode") -SPI_DATA_RATE_OPTIONS = { - 80e6: SPIDataRate.DATA_RATE_80MHZ, - 40e6: SPIDataRate.DATA_RATE_40MHZ, - 20e6: SPIDataRate.DATA_RATE_20MHZ, - 10e6: SPIDataRate.DATA_RATE_10MHZ, - 8e6: SPIDataRate.DATA_RATE_8MHZ, - 5e6: SPIDataRate.DATA_RATE_5MHZ, - 4e6: SPIDataRate.DATA_RATE_4MHZ, - 2e6: SPIDataRate.DATA_RATE_2MHZ, - 1e6: SPIDataRate.DATA_RATE_1MHZ, - 2e5: SPIDataRate.DATA_RATE_200KHZ, - 75e3: SPIDataRate.DATA_RATE_75KHZ, - 1e3: SPIDataRate.DATA_RATE_1KHZ, +PLATFORM_SPI_CLOCKS = { + PLATFORM_ESP8266: 40e6, + PLATFORM_ESP32: 80e6, + PLATFORM_RP2040: 62.5e6, } -SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS)) + +MAX_DATA_RATE_ERROR = 0.05 # Max allowable actual data rate difference from requested + + +def _render_hz(value: float) -> str: + """Render a frequency in Hz as a human-readable string using Hz, KHz or MHz. + + Examples: + 500 -> "500 Hz" + 1500 -> "1.5 kHz" + 2000000 -> "2 MHz" + """ + if value >= 1e6: + unit = "MHz" + num = value / 1e6 + elif value >= 1e3: + unit = "kHz" + num = value / 1e3 + else: + unit = "Hz" + num = value + + # Format with up to 2 decimal places, then strip unnecessary trailing zeros and dot + formatted = f"{int(num)}" if unit == "Hz" else f"{num:.2f}".rstrip("0").rstrip(".") + return formatted + unit + + +def _frequency_validator(value): + platform = get_target_platform() + frequency = PLATFORM_SPI_CLOCKS[platform] + value = cv.frequency(value) + if value > frequency: + raise cv.Invalid( + f"The configured SPI data rate ({_render_hz(value)}) exceeds the maximum for this platform ({_render_hz(frequency)})" + ) + if value < 1000: + raise cv.Invalid("The configured SPI data rate must be at least 1000Hz") + divisor = round(frequency / value) + actual = frequency / divisor + error = abs(actual - value) / value + if error > MAX_DATA_RATE_ERROR: + raise cv.Invalid( + f"The configured SPI data rate ({_render_hz(value)}) is not available for this chip - closest is {_render_hz(actual)}" + ) + return value + + +SPI_DATA_RATE_SCHEMA = _frequency_validator + SPI_MODE_OPTIONS = { "MODE0": SPIMode.MODE0, @@ -393,19 +432,20 @@ def spi_device_schema( :param mode Choose single, quad or octal mode. :return: The SPI device schema, `extend` this in your config schema. """ - schema = { - cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]), - cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA, - cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( - SPI_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32), - } - if cs_pin_required: - schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema - else: - schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema - return cv.Schema(schema) + cs_pin_option = cv.Required if cs_pin_required else cv.Optional + return cv.Schema( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]), + cv.Optional( + CONF_DATA_RATE, default=default_data_rate + ): SPI_DATA_RATE_SCHEMA, + cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32), + cs_pin_option(CONF_CS_PIN): pins.gpio_output_pin_schema, + } + ) async def register_spi_device(var, config): diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 00425b853f..b0c39033e3 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -329,6 +329,12 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) { } } void SSD1306::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index eb8d87998f..23576e7b2c 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -174,6 +174,12 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] |= color4; } void SSD1322::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); uint8_t fill = (color4 & SSD1322_COLORMASK) | ((color4 & SSD1322_COLORMASK) << SSD1322_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 6b83ec5f9d..2498bfcd67 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -150,6 +150,12 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] |= color4; } void SSD1327::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); uint8_t fill = (color4 & SSD1327_COLORMASK) | ((color4 & SSD1327_COLORMASK) << SSD1327_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index 8ee12387e4..a2993edef3 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -128,6 +128,12 @@ void HOT SSD1331::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] = color565 & 0xff; } void SSD1331::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color565 = display::ColorUtil::color_to_565(color); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { if (i & 1) { diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp index 09530e8a27..69bf67f476 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.cpp +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -160,6 +160,12 @@ void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] = color565 & 0xff; } void SSD1351::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color565 = display::ColorUtil::color_to_565(color); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { if (i & 1) { diff --git a/esphome/components/st7567_base/st7567_base.cpp b/esphome/components/st7567_base/st7567_base.cpp index 0afd2a70ba..8c47094b26 100644 --- a/esphome/components/st7567_base/st7567_base.cpp +++ b/esphome/components/st7567_base/st7567_base.cpp @@ -131,7 +131,16 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) { } } -void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } +void ST7567::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + + uint8_t fill = color.is_on() ? 0xFF : 0x00; + memset(buffer_, fill, this->get_buffer_length_()); +} void ST7567::init_reset_() { if (this->reset_pin_ != nullptr) { diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index c7ce7140e3..afd7cd61bd 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -89,7 +89,16 @@ void HOT ST7920::write_display_data() { } } -void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } +void ST7920::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + + uint8_t fill = color.is_on() ? 0xFF : 0x00; + memset(this->buffer_, fill, this->get_buffer_length_()); +} void ST7920::dump_config() { LOG_DISPLAY("", "ST7920", this); diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 937d9a5261..d341acb9d1 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -28,7 +28,7 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Required(CONF_TRIGGER_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, cv.Optional( diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index e864ea6419..184d93f189 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -1,64 +1,96 @@ #include "ultrasonic_sensor.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" -namespace esphome { -namespace ultrasonic { +namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) +static constexpr uint32_t TIMEOUT_MARGIN_US = 1000; // Extra margin for sensor processing time + +void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { + uint32_t now = micros(); + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + arg->echo_start_us = now; + arg->echo_start = true; + } else { + arg->echo_end_us = now; + arg->echo_end = true; + } +} + +void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { + InterruptLock lock; + this->store_.echo_start_us = 0; + this->store_.echo_end_us = 0; + this->store_.echo_start = false; + this->store_.echo_end = false; + this->trigger_pin_isr_.digital_write(true); + delayMicroseconds(this->pulse_time_us_); + this->trigger_pin_isr_.digital_write(false); + this->measurement_pending_ = true; + this->measurement_start_us_ = micros(); +} + void UltrasonicSensorComponent::setup() { this->trigger_pin_->setup(); this->trigger_pin_->digital_write(false); + this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - // isr is faster to access - echo_isr_ = echo_pin_->to_isr(); + this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } + void UltrasonicSensorComponent::update() { - this->trigger_pin_->digital_write(true); - delayMicroseconds(this->pulse_time_us_); - this->trigger_pin_->digital_write(false); + if (this->measurement_pending_) { + return; + } + this->send_trigger_pulse_(); +} - const uint32_t start = micros(); - while (micros() - start < timeout_us_ && echo_isr_.digital_read()) - ; - while (micros() - start < timeout_us_ && !echo_isr_.digital_read()) - ; - const uint32_t pulse_start = micros(); - while (micros() - start < timeout_us_ && echo_isr_.digital_read()) - ; - const uint32_t pulse_end = micros(); +void UltrasonicSensorComponent::loop() { + if (!this->measurement_pending_) { + return; + } - ESP_LOGV(TAG, "Echo took %" PRIu32 "µs", pulse_end - pulse_start); - - if (pulse_end - start >= timeout_us_) { - ESP_LOGD(TAG, "'%s' - Distance measurement timed out!", this->name_.c_str()); - this->publish_state(NAN); - } else { - float result = UltrasonicSensorComponent::us_to_m(pulse_end - pulse_start); + if (this->store_.echo_end) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); + this->measurement_pending_ = false; + return; + } + + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= this->timeout_us_ + TIMEOUT_MARGIN_US) { + ESP_LOGD(TAG, + "'%s' - Timeout after %" PRIu32 "us (measurement_start=%" PRIu32 ", echo_start=%" PRIu32 + ", echo_end=%" PRIu32 ")", + this->name_.c_str(), elapsed, this->measurement_start_us_, this->store_.echo_start_us, + this->store_.echo_end_us); + this->publish_state(NAN); + this->measurement_pending_ = false; } } + void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); ESP_LOGCONFIG(TAG, - " Pulse time: %" PRIu32 " µs\n" - " Timeout: %" PRIu32 " µs", + " Pulse time: %" PRIu32 " us\n" + " Timeout: %" PRIu32 " us", this->pulse_time_us_, this->timeout_us_); LOG_UPDATE_INTERVAL(this); } + float UltrasonicSensorComponent::us_to_m(uint32_t us) { const float speed_sound_m_per_s = 343.0f; const float time_s = us / 1e6f; const float total_dist = time_s * speed_sound_m_per_s; return total_dist / 2.0f; } -float UltrasonicSensorComponent::get_setup_priority() const { return setup_priority::DATA; } -void UltrasonicSensorComponent::set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } -void UltrasonicSensorComponent::set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } -} // namespace ultrasonic -} // namespace esphome +} // namespace esphome::ultrasonic diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 1a255d6122..e2266543ce 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -6,41 +6,49 @@ #include -namespace esphome { -namespace ultrasonic { +namespace esphome::ultrasonic { + +struct UltrasonicSensorStore { + static void gpio_intr(UltrasonicSensorStore *arg); + + volatile uint32_t echo_start_us{0}; + volatile uint32_t echo_end_us{0}; + volatile bool echo_start{false}; + volatile bool echo_end{false}; +}; class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent { public: - void set_trigger_pin(GPIOPin *trigger_pin) { trigger_pin_ = trigger_pin; } - void set_echo_pin(InternalGPIOPin *echo_pin) { echo_pin_ = echo_pin; } + void set_trigger_pin(InternalGPIOPin *trigger_pin) { this->trigger_pin_ = trigger_pin; } + void set_echo_pin(InternalGPIOPin *echo_pin) { this->echo_pin_ = echo_pin; } /// Set the timeout for waiting for the echo in µs. - void set_timeout_us(uint32_t timeout_us); + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - /// Set up pins and register interval. void setup() override; + void loop() override; void dump_config() override; - void update() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) - void set_pulse_time_us(uint32_t pulse_time_us); + void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } protected: /// Helper function to convert the specified echo duration in µs to meters. static float us_to_m(uint32_t us); - /// Helper function to convert the specified distance in meters to the echo duration in µs. + void send_trigger_pulse_(); - GPIOPin *trigger_pin_; + InternalGPIOPin *trigger_pin_; + ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; - ISRInternalGPIOPin echo_isr_; - uint32_t timeout_us_{}; /// 2 meters. + UltrasonicSensorStore store_; + uint32_t timeout_us_{}; uint32_t pulse_time_us_{}; + + uint32_t measurement_start_us_{0}; + bool measurement_pending_{false}; }; -} // namespace ultrasonic -} // namespace esphome +} // namespace esphome::ultrasonic diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index 1cf614286f..29120a3d0b 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -1,6 +1,7 @@ #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "usb_cdc_acm.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -16,6 +17,9 @@ namespace esphome::usb_cdc_acm { static const char *TAG = "usb_cdc_acm"; +// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168; + static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096; static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192; @@ -43,7 +47,10 @@ static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) { esp_err_t ret = tinyusb_cdcacm_read(static_cast(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size); ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size); - ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty(rx_buf, rx_size).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size)); if (ret == ESP_OK && rx_size > 0) { RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf(); @@ -306,7 +313,10 @@ void USBCDCACMInstance::usb_tx_task() { } ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size); - ESP_LOGVV(TAG, "data = %s", format_hex_pretty(data, tx_data_size).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size)); // Serial data will be split up into 64 byte chunks to be sent over USB so this // usually will take multiple iterations diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 9e058ee20b..e4c11be489 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -53,7 +53,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), - only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4]), + only_on_variant(supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3]), ) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index b946e3b38a..9bb5393be2 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -627,9 +627,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Assist Pipeline running"); #ifdef USE_MEDIA_PLAYER this->started_streaming_tts_ = false; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "url") { - this->tts_response_url_ = std::move(arg.value); + this->tts_response_url_ = arg.value; } } #endif @@ -648,9 +648,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; case api::enums::VOICE_ASSISTANT_STT_END: { std::string text; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "text") { - text = std::move(arg.value); + text = arg.value; } } if (text.empty()) { @@ -693,9 +693,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_INTENT_END: { - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "conversation_id") { - this->conversation_id_ = std::move(arg.value); + this->conversation_id_ = arg.value; } else if (arg.name == "continue_conversation") { this->continue_conversation_ = (arg.value == "1"); } @@ -705,9 +705,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_TTS_START: { std::string text; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "text") { - text = std::move(arg.value); + text = arg.value; } } if (text.empty()) { @@ -731,9 +731,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_TTS_END: { std::string url; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "url") { - url = std::move(arg.value); + url = arg.value; } } if (url.empty()) { @@ -778,11 +778,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_ERROR: { std::string code = ""; std::string message = ""; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "code") { - code = std::move(arg.value); + code = arg.value; } else if (arg.name == "message") { - message = std::move(arg.value); + message = arg.value; } } if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") { diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 3510d157d6..9ab050395d 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -172,6 +172,12 @@ void WaveshareEPaperBase::update() { this->display(); } void WaveshareEPaper::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + // flip logic const uint8_t fill = color.is_on() ? 0x00 : 0xFF; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) @@ -234,6 +240,12 @@ uint8_t WaveshareEPaper7C::color_to_hex(Color color) { return hex_code; } void WaveshareEPaper7C::fill(Color color) { + // If clipping is active, use base class (3-bit packing is complex for partial fills) + if (this->get_clipping().is_set()) { + display::Display::fill(color); + return; + } + uint8_t pixel_color; if (color.is_on()) { pixel_color = this->color_to_hex(color); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f613d6bc36..7c015adcf7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -45,62 +45,144 @@ static constexpr size_t PSTR_LOCAL_SIZE = 18; #define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1) // Parse URL and return match info -static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) { - UrlMatch match{}; +// URL formats (disambiguated by HTTP method for 3-segment case): +// GET /{domain}/{entity_name} - main device state +// POST /{domain}/{entity_name}/{action} - main device action +// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only) +// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only) +static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) { + // URL must start with '/' and have content after it + if (url_len < 2 || url_ptr[0] != '/') + return UrlMatch{}; - // URL must start with '/' - if (url_len < 2 || url_ptr[0] != '/') { - return match; - } - - // Skip leading '/' - const char *start = url_ptr + 1; + const char *p = url_ptr + 1; const char *end = url_ptr + url_len; - // Find domain (everything up to next '/' or end) - const char *domain_end = (const char *) memchr(start, '/', end - start); - if (!domain_end) { - // No second slash found - original behavior returns invalid - return match; - } + // Helper to find next segment: returns pointer after '/' or nullptr if no more slashes + auto next_segment = [&end](const char *start) -> const char * { + const char *slash = (const char *) memchr(start, '/', end - start); + return slash ? slash + 1 : nullptr; + }; - // Set domain - match.domain = start; - match.domain_len = domain_end - start; + // Helper to make StringRef from segment start to next segment (or end) + auto make_ref = [&end](const char *start, const char *next_start) -> StringRef { + return StringRef(start, (next_start ? next_start - 1 : end) - start); + }; + + // Parse domain segment + const char *s1 = p; + const char *s2 = next_segment(s1); + + // Must have domain with trailing slash + if (!s2) + return UrlMatch{}; + + UrlMatch match{}; + match.domain = make_ref(s1, s2); match.valid = true; - if (only_domain) { + if (only_domain || s2 >= end) return match; - } - // Parse ID if present - if (domain_end + 1 >= end) { - return match; // Nothing after domain slash - } + // Parse remaining segments only when needed + const char *s3 = next_segment(s2); + const char *s4 = s3 ? next_segment(s3) : nullptr; - const char *id_start = domain_end + 1; - const char *id_end = (const char *) memchr(id_start, '/', end - id_start); + StringRef seg2 = make_ref(s2, s3); + StringRef seg3 = s3 ? make_ref(s3, s4) : StringRef(); + StringRef seg4 = s4 ? make_ref(s4, nullptr) : StringRef(); - if (!id_end) { - // No more slashes, entire remaining string is ID - match.id = id_start; - match.id_len = end - id_start; - return match; - } + // Reject empty segments + if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty())) + return UrlMatch{}; - // Set ID - match.id = id_start; - match.id_len = id_end - id_start; - - // Parse method if present - if (id_end + 1 < end) { - match.method = id_end + 1; - match.method_len = end - (id_end + 1); + // Interpret based on segment count + if (!s3) { + // 1 segment after domain: /{domain}/{entity} + match.id = seg2; + } else if (!s4) { + // 2 segments after domain: /{domain}/{X}/{Y} + // HTTP method disambiguates: GET = device/entity, POST = entity/action + if (is_post) { + match.id = seg2; + match.method = seg3; + return match; + } +#ifdef USE_DEVICES + match.device_name = seg2; + match.id = seg3; +#else + return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES +#endif + } else { + // 3 segments after domain: /{domain}/{device}/{entity}/{action} +#ifdef USE_DEVICES + if (!is_post) { + return UrlMatch{}; // 4-segment GET not supported (action requires POST) + } + match.device_name = seg2; + match.id = seg3; + match.method = seg4; +#else + return UrlMatch{}; // Not supported without USE_DEVICES +#endif } return match; } +EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { + EntityMatchResult result{false, this->method.empty()}; + +#ifdef USE_DEVICES + Device *entity_device = entity->get_device(); + bool url_has_device = !this->device_name.empty(); + bool entity_has_device = (entity_device != nullptr); + + // Device matching: URL device segment must match entity's device + if (url_has_device != entity_has_device) { + return result; // Mismatch: one has device, other doesn't + } + if (url_has_device && this->device_name != entity_device->get_name()) { + return result; // Device name doesn't match + } +#endif + + // Try matching by entity name (new format) + if (this->id == entity->get_name()) { + result.matched = true; + return result; + } + + // Fall back to object_id (deprecated format) + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = entity->get_object_id_to(object_id_buf); + if (this->id == object_id) { + result.matched = true; + // Log deprecation warning +#ifdef USE_DEVICES + Device *device = entity->get_device(); + if (device != nullptr) { + ESP_LOGW(TAG, + "Deprecated URL format: /%.*s/%.*s/%.*s - use entity name '/%.*s/%s/%s' instead. " + "Object ID URLs will be removed in 2026.7.0.", + (int) this->domain.size(), this->domain.c_str(), (int) this->device_name.size(), + this->device_name.c_str(), (int) this->id.size(), this->id.c_str(), (int) this->domain.size(), + this->domain.c_str(), device->get_name(), entity->get_name().c_str()); + } else +#endif + { + ESP_LOGW(TAG, + "Deprecated URL format: /%.*s/%.*s - use entity name '/%.*s/%s' instead. " + "Object ID URLs will be removed in 2026.7.0.", + (int) this->domain.size(), this->domain.c_str(), (int) this->id.size(), this->id.c_str(), + (int) this->domain.size(), this->domain.c_str(), entity->get_name().c_str()); + } + } + + return result; +} + #if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { @@ -397,15 +479,53 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #endif // Helper functions to reduce code size by avoiding macro expansion +// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name} +// Uses names (not object_id) to avoid UTF-8 collision issues static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { - char id_buf[160]; // prefix + dash + object_id (up to 128) + null - size_t len = strlen(prefix); - memcpy(id_buf, prefix, len); // NOLINT(bugprone-not-null-terminated-result) - null added by write_object_id_to - id_buf[len++] = '-'; - obj->write_object_id_to(id_buf + len, sizeof(id_buf) - len); + const StringRef &name = obj->get_name(); + size_t prefix_len = strlen(prefix); + size_t name_len = name.size(); + +#ifdef USE_DEVICES + Device *device = obj->get_device(); + const char *device_name = device ? device->get_name() : nullptr; + size_t device_len = device_name ? strlen(device_name) : 0; +#endif + + // Build id into stack buffer - ArduinoJson copies the string + // Format: {prefix}/{device?}/{name} + // Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120): + // With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin + // Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin +#ifdef USE_DEVICES + char id_buf[280]; +#else + char id_buf[150]; +#endif + char *p = id_buf; + memcpy(p, prefix, prefix_len); + p += prefix_len; + *p++ = '/'; +#ifdef USE_DEVICES + if (device_name) { + memcpy(p, device_name, device_len); + p += device_len; + *p++ = '/'; + } +#endif + memcpy(p, name.c_str(), name_len); + p[name_len] = '\0'; + root[ESPHOME_F("id")] = id_buf; + if (start_config == DETAIL_ALL) { - root[ESPHOME_F("name")] = obj->get_name(); + root[ESPHOME_F("domain")] = prefix; + root[ESPHOME_F("name")] = name; +#ifdef USE_DEVICES + if (device_name) { + root[ESPHOME_F("device")] = device_name; + } +#endif root[ESPHOME_F("icon")] = obj->get_icon_ref(); root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); @@ -444,10 +564,11 @@ void WebServer::on_sensor_update(sensor::Sensor *obj) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -490,10 +611,11 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -532,10 +654,11 @@ void WebServer::on_switch_update(switch_::Switch *obj) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -601,9 +724,10 @@ std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -645,10 +769,11 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -686,10 +811,11 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -766,10 +892,11 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -844,10 +971,11 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -932,10 +1060,11 @@ void WebServer::on_number_update(number::Number *obj) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -999,9 +1128,10 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1062,9 +1192,10 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1124,9 +1255,10 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1188,10 +1320,11 @@ void WebServer::on_text_update(text::Text *obj) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -1244,10 +1377,11 @@ void WebServer::on_select_update(select::Select *obj) { } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); @@ -1301,10 +1435,11 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1451,10 +1586,11 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -1525,10 +1661,11 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1609,10 +1746,11 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); @@ -1690,11 +1828,12 @@ void WebServer::on_event(event::Event *obj) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->event_json_(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1759,10 +1898,11 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1973,7 +2113,8 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif // Parse URL for component routing - UrlMatch match = match_url(url.c_str(), url.length(), false); + // Pass HTTP method to disambiguate 3-segment URLs (GET=sub-device state, POST=main device action) + UrlMatch match = match_url(url.c_str(), url.length(), false, request->method() == HTTP_POST); // Route to appropriate handler based on domain // NOLINTNEXTLINE(readability-simplify-boolean-expr) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index b9e852c745..3e1dd867c6 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -35,33 +35,29 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE; namespace esphome::web_server { +/// Result of matching a URL against an entity +struct EntityMatchResult { + bool matched; ///< True if entity matched the URL + bool action_is_empty; ///< True if no action/method segment in URL +}; + /// Internal helper struct that is used to parse incoming URLs struct UrlMatch { - const char *domain; ///< Pointer to domain within URL, for example "sensor" - const char *id; ///< Pointer to id within URL, for example "living_room_fan" - const char *method; ///< Pointer to method within URL, for example "turn_on" - uint8_t domain_len; ///< Length of domain string - uint8_t id_len; ///< Length of id string - uint8_t method_len; ///< Length of method string - bool valid; ///< Whether this match is valid + StringRef domain; ///< Domain within URL, for example "sensor" + StringRef id; ///< Entity name/id within URL, for example "Temperature" + StringRef method; ///< Method within URL, for example "turn_on" +#ifdef USE_DEVICES + StringRef device_name; ///< Device name within URL, empty for main device +#endif + bool valid{false}; ///< Whether this match is valid // Helper methods for string comparisons - bool domain_equals(const char *str) const { - return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; - } + bool domain_equals(const char *str) const { return this->domain == str; } + bool method_equals(const char *str) const { return this->method == str; } - bool id_equals_entity(EntityBase *entity) const { - // Get object_id with zero heap allocation - char object_id_buf[OBJECT_ID_MAX_LEN]; - StringRef object_id = entity->get_object_id_to(object_id_buf); - return id && id_len == object_id.size() && memcmp(id, object_id.c_str(), id_len) == 0; - } - - bool method_equals(const char *str) const { - return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0; - } - - bool method_empty() const { return method_len == 0; } + /// Match entity by name first, then fall back to object_id with deprecation warning + /// Returns EntityMatchResult with match status and whether action segment is empty + EntityMatchResult match_entity(EntityBase *entity) const; }; #ifdef USE_WEBSERVER_SORTING diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index e27306ad78..a21e9cb9ff 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -5,6 +5,29 @@ namespace esphome::web_server { +// Write HTML-escaped text to stream (escapes ", &, <, >) +static void write_html_escaped(AsyncResponseStream *stream, const char *text) { + for (const char *p = text; *p; ++p) { + switch (*p) { + case '"': + stream->print("""); + break; + case '&': + stream->print("&"); + break; + case '<': + stream->print("<"); + break; + case '>': + stream->print(">"); + break; + default: + stream->write(*p); + break; + } + } +} + void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, const std::function &action_func = nullptr) { stream->print("print("-"); char object_id_buf[OBJECT_ID_MAX_LEN]; stream->print(obj->get_object_id_to(object_id_buf).c_str()); + // Add data attributes for hierarchical URL support + stream->print("\" data-domain=\""); + stream->print(klass.c_str()); + stream->print("\" data-name=\""); + write_html_escaped(stream, obj->get_name().c_str()); +#ifdef USE_DEVICES + Device *device = obj->get_device(); + if (device != nullptr) { + stream->print("\" data-device=\""); + write_html_escaped(stream, device->get_name()); + } +#endif stream->print("\">"); - stream->print(obj->get_name().c_str()); +#ifdef USE_DEVICES + if (device != nullptr) { + stream->print("["); + write_html_escaped(stream, device->get_name()); + stream->print("] "); + } +#endif + write_html_escaped(stream, obj->get_name().c_str()); stream->print(""); stream->print(action.c_str()); if (action_func) { diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index d5d34b520b..f27814062c 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -13,7 +13,8 @@ namespace web_server_idf { static const char *const TAG = "web_server_idf_utils"; -void url_decode(char *str) { +size_t url_decode(char *str) { + char *start = str; char *ptr = str, buf; for (; *str; str++, ptr++) { if (*str == '%') { @@ -31,7 +32,8 @@ void url_decode(char *str) { *ptr = *str; } } - *ptr = *str; + *ptr = '\0'; + return ptr - start; } bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index f70a5f0760..3a86aec7ac 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -8,6 +8,10 @@ namespace esphome { namespace web_server_idf { +/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space) +/// Returns the new length of the decoded string +size_t url_decode(char *str); + bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); optional request_get_url_query(httpd_req_t *req); diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 3d76b86a14..5062aa1e6c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -247,11 +247,20 @@ optional AsyncWebServerRequest::get_header(const char *name) const } std::string AsyncWebServerRequest::url() const { - auto *str = strchr(this->req_->uri, '?'); - if (str == nullptr) { - return this->req_->uri; + auto *query_start = strchr(this->req_->uri, '?'); + std::string result; + if (query_start == nullptr) { + result = this->req_->uri; + } else { + result = std::string(this->req_->uri, query_start - this->req_->uri); } - return std::string(this->req_->uri, str - this->req_->uri); + // Decode URL-encoded characters in-place (e.g., %20 -> space) + // This matches AsyncWebServer behavior on Arduino + if (!result.empty()) { + size_t new_len = url_decode(&result[0]); + result.resize(new_len); + } + return result; } std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ff2bfe12a4..5bf1f444e8 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -12,12 +12,6 @@ #include #include -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include -#include -#include -#endif - #ifdef USE_LIBRETINY #include #endif @@ -578,10 +572,6 @@ class WiFiComponent : public Component { static void s_wifi_scan_done_callback(void *arg, STATUS status); #endif -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); - void wifi_scan_done_callback_(); -#endif #ifdef USE_ESP32 void wifi_process_event_(IDFWiFiEvent *data); #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index b26ac3d2e2..5d4d003d62 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -483,6 +483,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { s_sta_connected = false; s_sta_connect_error = false; s_sta_connect_not_found = false; + // Reset IP address flags - ensures we don't report connected before DHCP completes + // (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect) + this->got_ipv4_address_ = false; +#if USE_NETWORK_IPV6 + this->num_ipv6_addresses_ = 0; +#endif err = esp_wifi_connect(); if (err != ESP_OK) { diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp index 4642768f90..d7f1ec3782 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgd1.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_cgd1 { static const char *const TAG = "xiaomi_cgd1"; +static constexpr size_t CGD1_BINDKEY_SIZE = 16; + void XiaomiCGD1::dump_config() { + char bindkey_hex[format_hex_pretty_size(CGD1_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi CGD1\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, CGD1_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp index 0dcbcbd05c..9151cbde41 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgdk2.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_cgdk2 { static const char *const TAG = "xiaomi_cgdk2"; +static constexpr size_t CGDK2_BINDKEY_SIZE = 16; + void XiaomiCGDK2::dump_config() { + char bindkey_hex[format_hex_pretty_size(CGDK2_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi CGDK2\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, CGDK2_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index f9fffa3f20..54b50a2eee 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgg1.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_cgg1 { static const char *const TAG = "xiaomi_cgg1"; +static constexpr size_t CGG1_BINDKEY_SIZE = 16; + void XiaomiCGG1::dump_config() { + char bindkey_hex[format_hex_pretty_size(CGG1_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi CGG1\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, CGG1_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp index dff1228f64..da5229c100 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -1,4 +1,5 @@ #include "xiaomi_lywsd02mmc.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_lywsd02mmc { static const char *const TAG = "xiaomi_lywsd02mmc"; +static constexpr size_t LYWSD02MMC_BINDKEY_SIZE = 16; + void XiaomiLYWSD02MMC::dump_config() { + char bindkey_hex[format_hex_pretty_size(LYWSD02MMC_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02MMC\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, LYWSD02MMC_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp index fb0165a21f..44fdb3b816 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -1,4 +1,5 @@ #include "xiaomi_lywsd03mmc.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_lywsd03mmc { static const char *const TAG = "xiaomi_lywsd03mmc"; +static constexpr size_t LYWSD03MMC_BINDKEY_SIZE = 16; + void XiaomiLYWSD03MMC::dump_config() { + char bindkey_hex[format_hex_pretty_size(LYWSD03MMC_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, LYWSD03MMC_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp index 90b654873b..55b81b301e 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -1,4 +1,5 @@ #include "xiaomi_mhoc401.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_mhoc401 { static const char *const TAG = "xiaomi_mhoc401"; +static constexpr size_t MHOC401_BINDKEY_SIZE = 16; + void XiaomiMHOC401::dump_config() { + char bindkey_hex[format_hex_pretty_size(MHOC401_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi MHOC401\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, MHOC401_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index 498e724368..112bf442e0 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -1,4 +1,5 @@ #include "xiaomi_rtcgq02lm.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,9 +9,12 @@ namespace xiaomi_rtcgq02lm { static const char *const TAG = "xiaomi_rtcgq02lm"; +static constexpr size_t RTCGQ02LM_BINDKEY_SIZE = 16; + void XiaomiRTCGQ02LM::dump_config() { + char bindkey_hex[format_hex_pretty_size(RTCGQ02LM_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi RTCGQ02LM"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, RTCGQ02LM_BINDKEY_SIZE, '.')); #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Motion", this->motion_); LOG_BINARY_SENSOR(" ", "Light", this->light_); diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp index f8712e7fd4..d3fec6cc9e 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp @@ -1,4 +1,5 @@ #include "xiaomi_xmwsdj04mmc.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,9 +9,12 @@ namespace xiaomi_xmwsdj04mmc { static const char *const TAG = "xiaomi_xmwsdj04mmc"; +static constexpr size_t XMWSDJ04MMC_BINDKEY_SIZE = 16; + void XiaomiXMWSDJ04MMC::dump_config() { + char bindkey_hex[format_hex_pretty_size(XMWSDJ04MMC_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, XMWSDJ04MMC_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index bd3f85772b..c1fde4de6b 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -12,6 +12,9 @@ namespace esphome::zwave_proxy { static const char *const TAG = "zwave_proxy"; +// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168; + static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; // GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value @@ -123,10 +126,11 @@ void ZWaveProxy::process_uart_() { } void ZWaveProxy::dump_config() { + char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; ESP_LOGCONFIG(TAG, "Z-Wave Proxy:\n" " Home ID: %s", - format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); } void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { @@ -167,7 +171,8 @@ bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) { return false; // No change } std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size()); - ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; + ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); this->home_id_ready_ = true; return true; // Home ID was changed } @@ -177,7 +182,10 @@ void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]); return; } - ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty(data, length).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty_to(hex_buf, data, length)); this->write_array(data, length); } @@ -250,7 +258,10 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK; } else { this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_ACK; - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_.data(), this->buffer_index_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_index_)); frame_completed = true; } this->response_handler_(); diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 137a1206e3..f36287d32a 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -14,6 +14,7 @@ namespace esphome::zwave_proxy { static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size +static constexpr size_t ZWAVE_HOME_ID_SIZE = 4; // Z-Wave Home ID size in bytes enum ZWaveResponseTypes : uint8_t { ZWAVE_FRAME_TYPE_ACK = 0x06, @@ -73,8 +74,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component { // Pre-allocated message - always ready to send api::ZWaveProxyFrame outgoing_proto_msg_; - std::array buffer_; // Fixed buffer for incoming data - std::array home_id_{0, 0, 0, 0}; // Fixed buffer for home ID + std::array buffer_; // Fixed buffer for incoming data + std::array home_id_{}; // Fixed buffer for home ID // Pointers and 32-bit values (aligned together) api::APIConnection *api_connection_{nullptr}; // Current subscribed client diff --git a/esphome/config_validation.py b/esphome/config_validation.py index d085206ee8..b0da88c50d 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1981,6 +1981,26 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( ) +def _validate_no_slash(value): + """Validate that a name does not contain '/' characters. + + The '/' character is used as a path separator in web server URLs, + so it cannot be used in entity or device names. + """ + if "/" in value: + raise Invalid( + f"Name cannot contain '/' character (used as URL path separator): {value}" + ) + return value + + +# Maximum length for entity, device, and area names +# This ensures web server URL IDs fit in a 280-byte buffer: +# domain(20) + "/" + device(120) + "/" + name(120) + null = 263 bytes +# Note: Must be < 255 because web_server UrlMatch uses uint8_t for length fields +NAME_MAX_LENGTH = 120 + + def _validate_entity_name(value): value = string(value) try: @@ -1991,9 +2011,28 @@ def _validate_entity_name(value): requires_friendly_name( "Name cannot be None when esphome->friendly_name is not set!" )(value) + if value is not None: + # Validate length for web server URL compatibility + if len(value) > NAME_MAX_LENGTH: + raise Invalid( + f"Name is too long ({len(value)} chars). " + f"Maximum length is {NAME_MAX_LENGTH} characters." + ) + # Validate no '/' in name for web server URL compatibility + _validate_no_slash(value) return value +def string_no_slash(value): + """Validate a string that cannot contain '/' characters. + + Used for device and area names where '/' is reserved as a URL path separator. + Use with cv.Length() to also enforce maximum length. + """ + value = string(value) + return _validate_no_slash(value) + + ENTITY_BASE_SCHEMA = Schema( { Optional(CONF_NAME): _validate_entity_name, diff --git a/esphome/core/config.py b/esphome/core/config.py index 5e32b9380d..f9c3011507 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -186,14 +186,14 @@ else: AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), - cv.Required(CONF_NAME): cv.string, + cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), } ) DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Device), - cv.Required(CONF_NAME): cv.string, + cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), cv.Optional(CONF_AREA_ID): cv.use_id(Area), } ) @@ -207,7 +207,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, - cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( + cv.string_no_slash, cv.Length(max=120) + ), cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), cv.Required(CONF_BUILD_PATH): cv.string, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a269f40479..579edc065a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -144,10 +144,7 @@ #define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_ONLINE_IMAGE_JPEG_SUPPORT #define USE_OTA -#define USE_OTA_MD5 #define USE_OTA_PASSWORD -#define USE_OTA_SHA256 -#define ALLOW_OTA_DOWNGRADE_MD5 #define USE_OTA_STATE_LISTENER #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE @@ -221,7 +218,7 @@ #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 5) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_MANUAL_IP diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 678040a04e..a45c7795bf 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -99,6 +99,8 @@ class EntityBase { return this->device_->get_device_id(); } void set_device(Device *device) { this->device_ = device; } + // Get the device this entity belongs to (nullptr if main device) + Device *get_device() const { return this->device_; } #endif // Check if this entity has state diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 0ccf21ad83..1a4230e421 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -3,20 +3,12 @@ #include #include "gpio.h" -#if defined(USE_ESP32_FRAMEWORK_ESP_IDF) +#if defined(USE_ESP32) #include #ifndef PROGMEM #define PROGMEM #endif -#elif defined(USE_ESP32_FRAMEWORK_ARDUINO) - -#include - -#ifndef PROGMEM -#define PROGMEM -#endif - #elif defined(USE_ESP8266) #include diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index c45c4df70b..0c1c2dce33 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -25,14 +25,8 @@ class HashBase { /// Retrieve the hash as bytes void get_bytes(uint8_t *output) { memcpy(output, this->digest_, this->get_size()); } - /// Retrieve the hash as hex characters - void get_hex(char *output) { - for (size_t i = 0; i < this->get_size(); i++) { - uint8_t byte = this->digest_[i]; - output[i * 2] = format_hex_char(byte >> 4); - output[i * 2 + 1] = format_hex_char(byte & 0x0F); - } - } + /// Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes. + void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); } /// Compare the hash against a provided byte-encoded hash bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 5e361ecce2..1c68f1a021 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -286,43 +286,60 @@ std::string format_mac_address_pretty(const uint8_t *mac) { return std::string(buf); } -std::string format_hex(const uint8_t *data, size_t length) { - std::string ret; - ret.resize(length * 2); - for (size_t i = 0; i < length; i++) { - ret[2 * i] = format_hex_char(data[i] >> 4); - ret[2 * i + 1] = format_hex_char(data[i] & 0x0F); +// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase +static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, + char base) { + if (length == 0) { + buffer[0] = '\0'; + return buffer; + } + // With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator) + // Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator) + uint8_t stride = separator ? 3 : 2; + size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); + if (max_bytes == 0) { + buffer[0] = '\0'; + return buffer; } - return ret; -} -std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } - -char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { - size_t max_bytes = (buffer_size - 1) / 2; if (length > max_bytes) { length = max_bytes; } for (size_t i = 0; i < length; i++) { - buffer[2 * i] = format_hex_char(data[i] >> 4); - buffer[2 * i + 1] = format_hex_char(data[i] & 0x0F); + size_t pos = i * stride; + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); + if (separator && i < length - 1) { + buffer[pos + 2] = separator; + } } - buffer[length * 2] = '\0'; + buffer[length * stride - (separator ? 1 : 0)] = '\0'; return buffer; } +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); +} + +std::string format_hex(const uint8_t *data, size_t length) { + std::string ret; + ret.resize(length * 2); + format_hex_to(&ret[0], length * 2 + 1, data, length); + return ret; +} +std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } + +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { + return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); +} + // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) return ""; std::string ret; - 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[multiple * i] = format_hex_pretty_char(data[i] >> 4); - ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); - if (separator && i != length - 1) - ret[multiple * i + 2] = separator; - } + size_t hex_len = separator ? (length * 3 - 1) : (length * 2); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); if (show_length && length > 4) return ret + " (" + std::to_string(length) + ")"; return ret; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 088214f3f2..f7a14ed2ec 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -691,12 +691,14 @@ constexpr uint8_t parse_hex_char(char c) { return 255; } +/// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) +inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } + /// Convert a nibble (0-15) to lowercase hex char -inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } +inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) -/// This always uses uppercase (A-F) for pretty/human-readable output -inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } +inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } /// Write int8 value to buffer without modulo operations. /// Buffer must have at least 4 bytes free. Returns pointer past last char written. @@ -722,28 +724,6 @@ inline char *int8_to_str(char *buf, int8_t val) { return buf; } -/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase) -inline void format_mac_addr_upper(const uint8_t *mac, char *output) { - for (size_t i = 0; i < 6; i++) { - uint8_t byte = mac[i]; - output[i * 3] = format_hex_pretty_char(byte >> 4); - output[i * 3 + 1] = format_hex_pretty_char(byte & 0x0F); - if (i < 5) - output[i * 3 + 2] = ':'; - } - output[17] = '\0'; -} - -/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators) -inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { - for (size_t i = 0; i < 6; i++) { - uint8_t byte = mac[i]; - output[i * 2] = format_hex_char(byte >> 4); - output[i * 2 + 1] = format_hex_char(byte & 0x0F); - } - output[12] = '\0'; -} - /// Format byte array as lowercase hex to buffer (base implementation). char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); @@ -762,6 +742,49 @@ inline char *format_hex_to(char (&buffer)[N], T val) { return format_hex_to(buffer, reinterpret_cast(&val), sizeof(T)); } +/// Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1 +constexpr size_t format_hex_size(size_t byte_count) { return byte_count * 2 + 1; } + +/// Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0" +constexpr size_t format_hex_pretty_size(size_t byte_count) { return byte_count * 3; } + +/** Format byte array as uppercase hex to buffer (base implementation). + * + * @param buffer Output buffer to write to. + * @param buffer_size Size of the output buffer. + * @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, or '\0' for no separator. + * @return Pointer to buffer. + * + * Buffer size needed: length * 3 with separator (for "XX:XX:XX\0"), length * 2 + 1 without. + */ +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator = ':'); + +/// Format byte array as uppercase hex with separator to buffer. Automatically deduces buffer size. +template +inline char *format_hex_pretty_to(char (&buffer)[N], const uint8_t *data, size_t length, char separator = ':') { + static_assert(N >= 3, "Buffer must hold at least one hex byte"); + return format_hex_pretty_to(buffer, N, data, length, separator); +} + +/// MAC address size in bytes +static constexpr size_t MAC_ADDRESS_SIZE = 6; +/// Buffer size for MAC address with separators: "XX:XX:XX:XX:XX:XX\0" +static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = format_hex_pretty_size(MAC_ADDRESS_SIZE); +/// Buffer size for MAC address without separators: "XXXXXXXXXXXX\0" +static constexpr size_t MAC_ADDRESS_BUFFER_SIZE = MAC_ADDRESS_SIZE * 2 + 1; + +/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators) +inline void format_mac_addr_upper(const uint8_t *mac, char *output) { + format_hex_pretty_to(output, MAC_ADDRESS_PRETTY_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE, ':'); +} + +/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators) +inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { + format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE); +} + /// Format the six-byte array \p mac into a MAC address. std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. @@ -1217,12 +1240,6 @@ class HighFrequencyLoopRequester { /// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes). void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) -/// Buffer size for MAC address in lowercase hex notation (12 hex chars + null terminator) -constexpr size_t MAC_ADDRESS_BUFFER_SIZE = 13; - -/// Buffer size for MAC address in colon-separated uppercase hex notation (17 chars + null terminator) -constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = 18; - /// Get the device MAC address as a string, in lowercase hex notation. std::string get_mac_address(); diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 68e2825d09..e96b739b58 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -1,12 +1,12 @@ #pragma once -#if defined(USE_ESP32) - #include #include +#ifdef USE_ESP32 #include #include +#endif /* * Lock-free queue for single-producer single-consumer scenarios. @@ -95,7 +95,7 @@ template class LockFreeQueue { } protected: - T *buffer_[SIZE]; + T *buffer_[SIZE]{}; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) std::atomic dropped_count_; // 65535 max - more than enough for drop tracking // Atomic: written by consumer (pop), read by producer (push) to check if full @@ -106,6 +106,7 @@ template class LockFreeQueue { std::atomic tail_; }; +#ifdef USE_ESP32 // Extended queue with task notification support template class NotifyingLockFreeQueue : public LockFreeQueue { public: @@ -140,7 +141,6 @@ template class NotifyingLockFreeQueue : public LockFreeQu private: TaskHandle_t task_to_notify_; }; +#endif } // namespace esphome - -#endif // defined(USE_ESP32) diff --git a/esphome/writer.py b/esphome/writer.py index 9ae40e417a..cb9c921693 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -103,14 +103,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool: def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: - if ( + # ESP32 uses CMake for both Arduino and ESP-IDF frameworks + return ( old.loaded_integrations != new.loaded_integrations or old.loaded_platforms != new.loaded_platforms - ) and new.core_platform == PLATFORM_ESP32: - from esphome.components.esp32 import FRAMEWORK_ESP_IDF - - return new.framework == FRAMEWORK_ESP_IDF - return False + ) and new.core_platform == PLATFORM_ESP32 def update_storage_json() -> None: diff --git a/platformio.ini b/platformio.ini index e38e1a5f3c..e58989c566 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.2/esp32-3.3.2.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5.zip framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -170,9 +170,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.1/esp-idf-v5.5.1.zip + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz framework = espidf lib_deps = diff --git a/requirements.txt b/requirements.txt index 65ff74a4a6..d457be9cd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==43.9.0 +aioesphomeapi==43.9.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.17 # dashboard_import @@ -22,7 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 -bleak==2.1.0 +bleak==2.1.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index cb09ef7050..f22b248747 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -374,20 +374,16 @@ def create_field_type_info( # Traditional fixed array approach with copy return FixedArrayBytesType(field, fixed_size) - # Check for pointer_to_buffer option on string fields - if field.type == 9: - has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False) - - if has_pointer_to_buffer: - # Zero-copy pointer approach for strings - return PointerToBytesBufferType(field, None) - # Special handling for bytes fields if field.type == 12: return BytesType(field, needs_decode, needs_encode) # Special handling for string fields if field.type == 9: + # For SOURCE_CLIENT only messages (decode but no encode), use StringRef + # for zero-copy access to the receive buffer + if needs_decode and not needs_encode: + return PointerToStringBufferType(field, None) return StringType(field, needs_decode, needs_encode) validate_field_type(field.type, field.name) @@ -840,8 +836,8 @@ class BytesType(TypeInfo): return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes -class PointerToBytesBufferType(TypeInfo): - """Type for bytes fields that use pointer_to_buffer option for zero-copy.""" +class PointerToBufferTypeBase(TypeInfo): + """Base class for pointer_to_buffer types (bytes and strings) for zero-copy decoding.""" @classmethod def can_use_dump_field(cls) -> bool: @@ -851,29 +847,34 @@ class PointerToBytesBufferType(TypeInfo): self, field: descriptor.FieldDescriptorProto, size: int | None = None ) -> None: super().__init__(field) - # Size is not used for pointer_to_buffer - we always use size_t for length self.array_size = 0 @property - def cpp_type(self) -> str: - return "const uint8_t*" + def decode_length(self) -> str | None: + # This is handled in decode_length_content + return None @property - def default_value(self) -> str: - return "nullptr" + def wire_type(self) -> WireType: + """Get the wire type for this field.""" + return WireType.LENGTH_DELIMITED # Uses wire type 2 - @property - def reference_type(self) -> str: - return "const uint8_t*" + def get_estimated_size(self) -> int: + # field ID + length varint + typical data (assume small for pointer fields) + return self.calculate_field_id_size() + 2 + 16 - @property - def const_reference_type(self) -> str: - return "const uint8_t*" + +class PointerToBytesBufferType(PointerToBufferTypeBase): + """Type for bytes fields that use pointer_to_buffer option for zero-copy.""" + + cpp_type = "const uint8_t*" + default_value = "nullptr" + reference_type = "const uint8_t*" + const_reference_type = "const uint8_t*" @property def public_content(self) -> list[str]: # Use uint16_t for length - max packet size is well below 65535 - # Add pointer and length fields return [ f"const uint8_t* {self.field_name}{{nullptr}};", f"uint16_t {self.field_name}_len{{0}};", @@ -885,24 +886,12 @@ class PointerToBytesBufferType(TypeInfo): @property def decode_length_content(self) -> str | None: - # Decode directly stores the pointer to avoid allocation return f"""case {self.number}: {{ - // Use raw data directly to avoid allocation this->{self.field_name} = value.data(); this->{self.field_name}_len = value.size(); break; }}""" - @property - def decode_length(self) -> str | None: - # This is handled in decode_length_content - return None - - @property - def wire_type(self) -> WireType: - """Get the wire type for this bytes field.""" - return WireType.LENGTH_DELIMITED # Uses wire type 2 - def dump(self, name: str) -> str: return ( f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)" @@ -910,7 +899,6 @@ class PointerToBytesBufferType(TypeInfo): @property def dump_content(self) -> str: - # Custom dump that doesn't use dump_field template return ( f'out.append(" {self.name}: ");\n' + f"out.append({self.dump(self.field_name)});\n" @@ -918,11 +906,48 @@ class PointerToBytesBufferType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.number}, this->{self.field_name}_len);" + return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" - def get_estimated_size(self) -> int: - # field ID + length varint + typical data (assume small for pointer fields) - return self.calculate_field_id_size() + 2 + 16 + +class PointerToStringBufferType(PointerToBufferTypeBase): + """Type for string fields that use pointer_to_buffer option for zero-copy. + + Uses StringRef instead of separate pointer and length fields. + """ + + cpp_type = "StringRef" + default_value = "" + reference_type = "StringRef &" + const_reference_type = "const StringRef &" + + @property + def public_content(self) -> list[str]: + return [f"StringRef {self.field_name}{{}};"] + + @property + def encode_content(self) -> str: + return f"buffer.encode_string({self.number}, this->{self.field_name});" + + @property + def decode_length_content(self) -> str | None: + return f"""case {self.number}: {{ + this->{self.field_name} = StringRef(reinterpret_cast(value.data()), value.size()); + break; + }}""" + + def dump(self, name: str) -> str: + return f'out.append("\'").append(this->{self.field_name}.c_str(), this->{self.field_name}.size()).append("\'");' + + @property + def dump_content(self) -> str: + return ( + f'out.append(" {self.name}: ");\n' + + f"{self.dump(self.field_name)}\n" + + 'out.append("\\n");' + ) + + def get_size_calculation(self, name: str, force: bool = False) -> str: + return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" class FixedArrayBytesType(TypeInfo): diff --git a/tests/component_tests/mipi_spi/conftest.py b/tests/component_tests/mipi_spi/conftest.py index c3070c7965..eddf0987d0 100644 --- a/tests/component_tests/mipi_spi/conftest.py +++ b/tests/component_tests/mipi_spi/conftest.py @@ -20,9 +20,9 @@ def choose_variant_with_pins() -> Generator[Callable[[list], None]]: """ def chooser(pins: list) -> None: - for v in VARIANTS: + for variant in VARIANTS: try: - CORE.data[KEY_ESP32][KEY_VARIANT] = v + CORE.data[KEY_ESP32][KEY_VARIANT] = variant for pin in pins: if pin is not None: pin = gpio_pin_schema( diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index f752c41d8c..bae39d3879 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -1,4 +1,4 @@ -"""Tests for mpip_spi configuration validation.""" +"""Tests for mipi_spi configuration validation.""" from collections.abc import Callable from pathlib import Path diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index 32fb4efb04..2c99e937f3 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -11,3 +11,4 @@ display: dc_pin: 21 reset_pin: 22 invert_colors: false + data_rate: 10MHz diff --git a/tests/components/bme68x_bsec2_i2c/common.yaml b/tests/components/bme68x_bsec2_i2c/common.yaml index bee964f433..a462bdaf7f 100644 --- a/tests/components/bme68x_bsec2_i2c/common.yaml +++ b/tests/components/bme68x_bsec2_i2c/common.yaml @@ -9,6 +9,7 @@ bme68x_bsec2_i2c: sensor: - platform: bme68x_bsec2 + id: bme_sensor temperature: name: BME68X Temperature pressure: diff --git a/tests/components/bthome_mithermometer/common.yaml b/tests/components/bthome_mithermometer/common.yaml new file mode 100644 index 0000000000..ba94e46878 --- /dev/null +++ b/tests/components/bthome_mithermometer/common.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: bthome_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: "BTHome Temperature" + humidity: + name: "BTHome Humidity" + battery_level: + name: "BTHome Battery" + battery_voltage: + name: "BTHome Battery Voltage" + signal_strength: + name: "BTHome Signal" diff --git a/tests/components/bthome_mithermometer/test.esp32-idf.yaml b/tests/components/bthome_mithermometer/test.esp32-idf.yaml new file mode 100644 index 0000000000..7a6541ae76 --- /dev/null +++ b/tests/components/bthome_mithermometer/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/chsc6x/test.rp2040-ard.yaml b/tests/components/chsc6x/test.rp2040-ard.yaml index 2e3613a4a3..eb21b8ec4b 100644 --- a/tests/components/chsc6x/test.rp2040-ard.yaml +++ b/tests/components/chsc6x/test.rp2040-ard.yaml @@ -9,6 +9,7 @@ display: invert_colors: True cs_pin: 20 dc_pin: 21 + data_rate: 20MHz pages: - id: page1 lambda: |- diff --git a/tests/components/display/common.yaml b/tests/components/display/common.yaml index 27abb23e03..a722a5f7c2 100644 --- a/tests/components/display/common.yaml +++ b/tests/components/display/common.yaml @@ -6,6 +6,7 @@ display: dc_pin: 13 reset_pin: 21 invert_colors: false + data_rate: 20MHz lambda: |- // Draw an analog clock in the center of the screen int centerX = it.get_width() / 2; diff --git a/tests/components/ili9xxx/common.yaml b/tests/components/ili9xxx/common.yaml index 47384b1398..4665e55e4b 100644 --- a/tests/components/ili9xxx/common.yaml +++ b/tests/components/ili9xxx/common.yaml @@ -11,6 +11,7 @@ display: cs_pin: ${cs_pin1} dc_pin: ${dc_pin1} reset_pin: ${reset_pin1} + data_rate: 20MHz lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx @@ -27,5 +28,6 @@ display: reset_pin: ${reset_pin2} auto_clear_enabled: false rotation: 90 + data_rate: 20MHz lambda: |- it.fill(Color::WHITE); diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml index 40f17d57fe..03a9c42a38 100644 --- a/tests/components/image/test.rp2040-ard.yaml +++ b/tests/components/image/test.rp2040-ard.yaml @@ -9,6 +9,7 @@ display: cs_pin: 20 dc_pin: 21 reset_pin: 22 + data_rate: 20MHz invert_colors: true <<: !include common.yaml diff --git a/tests/components/online_image/common-rp2040.yaml b/tests/components/online_image/common-rp2040.yaml index 25891b94bc..bbb514bded 100644 --- a/tests/components/online_image/common-rp2040.yaml +++ b/tests/components/online_image/common-rp2040.yaml @@ -8,6 +8,7 @@ display: spi_id: spi_bus id: main_lcd model: ili9342 + data_rate: 20MHz cs_pin: 20 dc_pin: 17 reset_pin: 21 diff --git a/tests/components/qr_code/common.yaml b/tests/components/qr_code/common.yaml index 15b4e387c6..2ffba67763 100644 --- a/tests/components/qr_code/common.yaml +++ b/tests/components/qr_code/common.yaml @@ -5,6 +5,7 @@ display: cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + data_rate: 500kHz invert_colors: false lambda: |- // Draw a QR code in the center of the screen diff --git a/tests/components/xpt2046/common.yaml b/tests/components/xpt2046/common.yaml index 3a8e3286a6..eddbd24d6a 100644 --- a/tests/components/xpt2046/common.yaml +++ b/tests/components/xpt2046/common.yaml @@ -6,6 +6,7 @@ display: cs_pin: ${disp_cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + data_rate: 20MHz invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index c9d7b7486e..94224f2364 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -502,3 +502,60 @@ def test_only_with_user_value_overrides_default() -> None: result = schema({"mqtt_id": "custom_id"}) assert result.get("mqtt_id") == "custom_id" + + +@pytest.mark.parametrize("value", ("hello", "Hello World", "test_name", "温度")) +def test_string_no_slash__valid(value: str) -> None: + actual = config_validation.string_no_slash(value) + assert actual == value + + +@pytest.mark.parametrize("value", ("has/slash", "a/b/c", "/leading", "trailing/")) +def test_string_no_slash__slash_rejected(value: str) -> None: + with pytest.raises(Invalid, match="cannot contain '/' character"): + config_validation.string_no_slash(value) + + +def test_string_no_slash__long_string_allowed() -> None: + # string_no_slash doesn't enforce length - use cv.Length() separately + long_value = "x" * 200 + assert config_validation.string_no_slash(long_value) == long_value + + +def test_string_no_slash__empty() -> None: + assert config_validation.string_no_slash("") == "" + + +@pytest.mark.parametrize("value", ("Temperature", "Living Room Light", "温度传感器")) +def test_validate_entity_name__valid(value: str) -> None: + actual = config_validation._validate_entity_name(value) + assert actual == value + + +def test_validate_entity_name__slash_rejected() -> None: + with pytest.raises(Invalid, match="cannot contain '/' character"): + config_validation._validate_entity_name("has/slash") + + +def test_validate_entity_name__max_length() -> None: + # 120 chars should pass + assert config_validation._validate_entity_name("x" * 120) == "x" * 120 + + # 121 chars should fail + with pytest.raises(Invalid, match="too long.*121 chars.*Maximum.*120"): + config_validation._validate_entity_name("x" * 121) + + +def test_validate_entity_name__none_without_friendly_name() -> None: + # When name is "None" and friendly_name is not set, it should fail + CORE.friendly_name = None + with pytest.raises(Invalid, match="friendly_name is not set"): + config_validation._validate_entity_name("None") + + +def test_validate_entity_name__none_with_friendly_name() -> None: + # When name is "None" but friendly_name is set, it should return None + CORE.friendly_name = "My Device" + result = config_validation._validate_entity_name("None") + assert result is None + CORE.friendly_name = None # Reset diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index f354d71bb7..ac05e0d31b 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -13,6 +13,13 @@ from unittest.mock import MagicMock, patch import pytest +from esphome.const import ( + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, + PLATFORM_RTL87XX, +) from esphome.core import EsphomeError from esphome.storage_json import StorageJSON from esphome.writer import ( @@ -28,6 +35,7 @@ from esphome.writer import ( generate_build_info_data_h, get_build_info, storage_should_clean, + storage_should_update_cmake_cache, update_storage_json, write_cpp, write_gitignore, @@ -171,6 +179,86 @@ def test_storage_edge_case_from_empty_integrations( assert storage_should_clean(old, new) is False +# Tests for storage_should_update_cmake_cache + + +@pytest.mark.parametrize("framework", ["arduino", "esp-idf"]) +def test_storage_should_update_cmake_cache_when_integration_added_esp32( + create_storage: Callable[..., StorageJSON], + framework: str, +) -> None: + """Test cmake cache update triggered when integration added on ESP32.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=PLATFORM_ESP32, + framework=framework, + ) + new = create_storage( + loaded_integrations=["api", "wifi", "restart"], + core_platform=PLATFORM_ESP32, + framework=framework, + ) + assert storage_should_update_cmake_cache(old, new) is True + + +def test_storage_should_update_cmake_cache_when_platform_changed_esp32( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test cmake cache update triggered when platforms change on ESP32.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + loaded_platforms={"sensor"}, + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + new = create_storage( + loaded_integrations=["api", "wifi"], + loaded_platforms={"sensor", "binary_sensor"}, + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + assert storage_should_update_cmake_cache(old, new) is True + + +def test_storage_should_not_update_cmake_cache_when_nothing_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test cmake cache not updated when nothing changes.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + new = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + assert storage_should_update_cmake_cache(old, new) is False + + +@pytest.mark.parametrize( + "core_platform", + [PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX], +) +def test_storage_should_not_update_cmake_cache_for_non_esp32( + create_storage: Callable[..., StorageJSON], + core_platform: str, +) -> None: + """Test cmake cache not updated for non-ESP32 platforms.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=core_platform, + framework="arduino", + ) + new = create_storage( + loaded_integrations=["api", "wifi", "restart"], + core_platform=core_platform, + framework="arduino", + ) + assert storage_should_update_cmake_cache(old, new) is False + + @patch("esphome.writer.clean_build") @patch("esphome.writer.StorageJSON") @patch("esphome.writer.storage_path")