From ce5e1a6294b90cc0e1167038d3e857b25185e682 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 09:40:58 -0500 Subject: [PATCH 001/193] Bump setuptools from 79.0.1 to 80.3.1 (#8696) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e3b10722c8..1910f008ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==79.0.1", "wheel>=0.43,<0.46"] +requires = ["setuptools==80.3.1", "wheel>=0.43,<0.46"] build-backend = "setuptools.build_meta" [project] From 4d43caf6c174680396ac85af218b583fc977c2e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 09:41:14 -0500 Subject: [PATCH 002/193] Bump aioesphomeapi from 30.0.1 to 30.1.0 (#8652) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f09d7894dd..1f71ce9b16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250415.0 -aioesphomeapi==30.0.1 +aioesphomeapi==30.1.0 zeroconf==0.146.5 puremagic==1.28 ruamel.yaml==0.18.10 # dashboard_import From 39b119e9ccb32e9a03a2292c4363ca86037b25a3 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 6 May 2025 16:48:56 -0500 Subject: [PATCH 003/193] [micro_wake_word] Experimental cutoff adjustments and uses mic sample rate (#8702) --- .../micro_wake_word/micro_wake_word.cpp | 18 ++++++++++-------- .../micro_wake_word/micro_wake_word.h | 2 -- .../micro_wake_word/preprocessor_settings.h | 2 -- .../micro_wake_word/streaming_model.cpp | 10 ++++++---- .../micro_wake_word/streaming_model.h | 19 +++++++++++++------ 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 46ca328730..a44348fdc9 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -22,8 +22,6 @@ static const ssize_t DETECTION_QUEUE_LENGTH = 5; static const size_t DATA_TIMEOUT_MS = 50; static const uint32_t RING_BUFFER_DURATION_MS = 120; -static const uint32_t RING_BUFFER_SAMPLES = RING_BUFFER_DURATION_MS * (AUDIO_SAMPLE_FREQUENCY / 1000); -static const size_t RING_BUFFER_SIZE = RING_BUFFER_SAMPLES * sizeof(int16_t); static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072; static const UBaseType_t INFERENCE_TASK_PRIORITY = 3; @@ -141,13 +139,15 @@ void MicroWakeWord::inference_task(void *params) { xEventGroupSetBits(this_mww->event_group_, EventGroupBits::TASK_STARTING); { // Ensures any C++ objects fall out of scope to deallocate before deleting the task - const size_t new_samples_to_read = this_mww->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000); + + const size_t new_bytes_to_process = + this_mww->microphone_source_->get_audio_stream_info().ms_to_bytes(this_mww->features_step_size_); std::unique_ptr audio_buffer; int8_t features_buffer[PREPROCESSOR_FEATURE_SIZE]; if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) { // Allocate audio transfer buffer - audio_buffer = audio::AudioSourceTransferBuffer::create(new_samples_to_read * sizeof(int16_t)); + audio_buffer = audio::AudioSourceTransferBuffer::create(new_bytes_to_process); if (audio_buffer == nullptr) { xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY); @@ -156,7 +156,8 @@ void MicroWakeWord::inference_task(void *params) { if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) { // Allocate ring buffer - std::shared_ptr temp_ring_buffer = RingBuffer::create(RING_BUFFER_SIZE); + std::shared_ptr temp_ring_buffer = RingBuffer::create( + this_mww->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); if (temp_ring_buffer.use_count() == 0) { xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY); } @@ -171,13 +172,13 @@ void MicroWakeWord::inference_task(void *params) { while (!(xEventGroupGetBits(this_mww->event_group_) & COMMAND_STOP)) { audio_buffer->transfer_data_from_source(pdMS_TO_TICKS(DATA_TIMEOUT_MS)); - if (audio_buffer->available() < new_samples_to_read * sizeof(int16_t)) { + if (audio_buffer->available() < new_bytes_to_process) { // Insufficient data to generate new spectrogram features, read more next iteration continue; } // Generate new spectrogram features - size_t processed_samples = this_mww->generate_features_( + uint32_t processed_samples = this_mww->generate_features_( (int16_t *) audio_buffer->get_buffer_start(), audio_buffer->available() / sizeof(int16_t), features_buffer); audio_buffer->decrease_buffer_length(processed_samples * sizeof(int16_t)); @@ -297,7 +298,8 @@ void MicroWakeWord::loop() { if ((this->inference_task_handle_ == nullptr) && !this->status_has_error()) { // Setup preprocesor feature generator. If done in the task, it would lock the task to its initial core, as it // uses floating point operations. - if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, AUDIO_SAMPLE_FREQUENCY)) { + if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, + this->microphone_source_->get_audio_stream_info().get_sample_rate())) { this->status_momentary_error( "Failed to allocate buffers for spectrogram feature processor, attempting again in 1 second", 1000); return; diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index 626b8bffb8..d46c40e48b 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -121,8 +121,6 @@ class MicroWakeWord : public Component { /// @param audio_features (int8_t *) Buffer containing new spectrogram features /// @return True if successful, false if any errors were encountered bool update_model_probabilities_(const int8_t audio_features[PREPROCESSOR_FEATURE_SIZE]); - - inline uint16_t new_samples_to_get_() { return (this->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000)); } }; } // namespace micro_wake_word diff --git a/esphome/components/micro_wake_word/preprocessor_settings.h b/esphome/components/micro_wake_word/preprocessor_settings.h index 025e21c5f7..3de21de92e 100644 --- a/esphome/components/micro_wake_word/preprocessor_settings.h +++ b/esphome/components/micro_wake_word/preprocessor_settings.h @@ -15,8 +15,6 @@ namespace micro_wake_word { static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40; // Duration of each slice used as input into the preprocessor static const uint8_t FEATURE_DURATION_MS = 30; -// Audio sample frequency in hertz -static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000; static const float FILTERBANK_LOWER_BAND_LIMIT = 125.0; static const float FILTERBANK_UPPER_BAND_LIMIT = 7500.0; diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 6512c0f569..ce3d8c2e4c 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -159,12 +159,13 @@ void StreamingModel::reset_probabilities() { this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION; } -WakeWordModel::WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t probability_cutoff, +WakeWordModel::WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_average_size, const std::string &wake_word, size_t tensor_arena_size, bool default_enabled, bool internal_only) { this->id_ = id; this->model_start_ = model_start; - this->probability_cutoff_ = probability_cutoff; + this->default_probability_cutoff_ = default_probability_cutoff; + this->probability_cutoff_ = default_probability_cutoff; this->sliding_window_size_ = sliding_window_average_size; this->recent_streaming_probabilities_.resize(sliding_window_average_size, 0); this->wake_word_ = wake_word; @@ -222,10 +223,11 @@ DetectionEvent WakeWordModel::determine_detected() { return detection_event; } -VADModel::VADModel(const uint8_t *model_start, uint8_t probability_cutoff, size_t sliding_window_size, +VADModel::VADModel(const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_size, size_t tensor_arena_size) { this->model_start_ = model_start; - this->probability_cutoff_ = probability_cutoff; + this->default_probability_cutoff_ = default_probability_cutoff; + this->probability_cutoff_ = default_probability_cutoff; this->sliding_window_size_ = sliding_window_size; this->recent_streaming_probabilities_.resize(sliding_window_size, 0); this->tensor_arena_size_ = tensor_arena_size; diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index 5bd1cf356a..b7b22b9700 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -50,9 +50,14 @@ class StreamingModel { virtual void disable() { this->enabled_ = false; } /// @brief Return true if the model is enabled. - bool is_enabled() { return this->enabled_; } + bool is_enabled() const { return this->enabled_; } - bool get_unprocessed_probability_status() { return this->unprocessed_probability_status_; } + bool get_unprocessed_probability_status() const { return this->unprocessed_probability_status_; } + + // Quantized probability cutoffs mapping 0.0 - 1.0 to 0 - 255 + uint8_t get_default_probability_cutoff() const { return this->default_probability_cutoff_; } + uint8_t get_probability_cutoff() const { return this->probability_cutoff_; } + void set_probability_cutoff(uint8_t probability_cutoff) { this->probability_cutoff_ = probability_cutoff; } protected: /// @brief Allocates tensor and variable arenas and sets up the model interpreter @@ -69,8 +74,10 @@ class StreamingModel { uint8_t current_stride_step_{0}; int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION}; - uint8_t probability_cutoff_; // Quantized probability cutoff mapping 0.0 - 1.0 to 0 - 255 + uint8_t default_probability_cutoff_; + uint8_t probability_cutoff_; size_t sliding_window_size_; + size_t last_n_index_{0}; size_t tensor_arena_size_; std::vector recent_streaming_probabilities_; @@ -88,14 +95,14 @@ class WakeWordModel final : public StreamingModel { /// @brief Constructs a wake word model object /// @param id (std::string) identifier for this model /// @param model_start (const uint8_t *) pointer to the start of the model's TFLite FlatBuffer - /// @param probability_cutoff (uint8_t) probability cutoff for acceping the wake word has been said + /// @param default_probability_cutoff (uint8_t) probability cutoff for acceping the wake word has been said /// @param sliding_window_average_size (size_t) the length of the sliding window computing the mean rolling /// probability /// @param wake_word (std::string) Friendly name of the wake word /// @param tensor_arena_size (size_t) Size in bytes for allocating the tensor arena /// @param default_enabled (bool) If true, it will be enabled by default on first boot /// @param internal_only (bool) If true, the model will not be exposed to HomeAssistant as an available model - WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t probability_cutoff, + WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_average_size, const std::string &wake_word, size_t tensor_arena_size, bool default_enabled, bool internal_only); @@ -132,7 +139,7 @@ class WakeWordModel final : public StreamingModel { class VADModel final : public StreamingModel { public: - VADModel(const uint8_t *model_start, uint8_t probability_cutoff, size_t sliding_window_size, + VADModel(const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_size, size_t tensor_arena_size); void log_model_config() override; From 75496849ebe48e4059fbe3b48a0076d6890cc132 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 May 2025 11:57:18 +1200 Subject: [PATCH 004/193] [mics_4514] Add default device class to CO sensor (#8710) --- esphome/components/mics_4514/sensor.py | 39 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/esphome/components/mics_4514/sensor.py b/esphome/components/mics_4514/sensor.py index 59ccba235a..09329ebfcf 100644 --- a/esphome/components/mics_4514/sensor.py +++ b/esphome/components/mics_4514/sensor.py @@ -9,6 +9,8 @@ from esphome.const import ( CONF_ID, CONF_METHANE, CONF_NITROGEN_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) @@ -22,24 +24,33 @@ MICS4514Component = mics_4514_ns.class_( "MICS4514Component", cg.PollingComponent, i2c.I2CDevice ) -SENSORS = [ - CONF_CARBON_MONOXIDE, - CONF_METHANE, - CONF_ETHANOL, - CONF_HYDROGEN, - CONF_AMMONIA, - CONF_NITROGEN_DIOXIDE, -] +SENSORS = { + CONF_CARBON_MONOXIDE: DEVICE_CLASS_CARBON_MONOXIDE, + CONF_METHANE: DEVICE_CLASS_EMPTY, + CONF_ETHANOL: DEVICE_CLASS_EMPTY, + CONF_HYDROGEN: DEVICE_CLASS_EMPTY, + CONF_AMMONIA: DEVICE_CLASS_EMPTY, + CONF_NITROGEN_DIOXIDE: DEVICE_CLASS_EMPTY, +} + + +def common_sensor_schema(*, device_class: str) -> cv.Schema: + return sensor.sensor_schema( + accuracy_decimals=2, + device_class=device_class, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + ) -common_sensor_schema = sensor.sensor_schema( - unit_of_measurement=UNIT_PARTS_PER_MILLION, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=2, -) CONFIG_SCHEMA = ( cv.Schema({cv.GenerateID(): cv.declare_id(MICS4514Component)}) - .extend({cv.Optional(sensor_type): common_sensor_schema for sensor_type in SENSORS}) + .extend( + { + cv.Optional(sensor_type): common_sensor_schema(device_class=device_class) + for sensor_type, device_class in SENSORS.items() + } + ) .extend(i2c.i2c_device_schema(0x75)) .extend(cv.polling_component_schema("60s")) ) From e9887625768f9c0146a857a55d5895c34cbf057a Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 6 May 2025 23:42:59 -0500 Subject: [PATCH 005/193] [i2s_audio, mixer, resampler, speaker] Simplify duration played callback (#8703) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 18 +++------- .../mixer/speaker/mixer_speaker.cpp | 34 +++++++------------ .../components/mixer/speaker/mixer_speaker.h | 4 +-- .../resampler/speaker/resampler_speaker.cpp | 22 ++++++------ .../resampler/speaker/resampler_speaker.h | 2 +- .../media_player/speaker_media_player.cpp | 18 ---------- .../media_player/speaker_media_player.h | 11 ------ esphome/components/speaker/speaker.h | 11 +++--- 8 files changed, 35 insertions(+), 85 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 7d247003f7..b287177016 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -14,6 +14,8 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esp_timer.h" + namespace esphome { namespace i2s_audio { @@ -366,25 +368,15 @@ void I2SAudioSpeaker::speaker_task(void *params) { bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); #endif - uint32_t write_timestamp = micros(); + int64_t now = esp_timer_get_time(); if (bytes_written != bytes_to_write) { xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); } - bytes_read -= bytes_written; - this_speaker->accumulated_frames_written_ += audio_stream_info.bytes_to_frames(bytes_written); - const uint32_t new_playback_ms = - audio_stream_info.frames_to_milliseconds_with_remainder(&this_speaker->accumulated_frames_written_); - const uint32_t remainder_us = - audio_stream_info.frames_to_microseconds(this_speaker->accumulated_frames_written_); - - uint32_t pending_frames = - audio_stream_info.bytes_to_frames(bytes_read + this_speaker->audio_ring_buffer_->available()); - const uint32_t pending_ms = audio_stream_info.frames_to_milliseconds_with_remainder(&pending_frames); - - this_speaker->audio_output_callback_(new_playback_ms, remainder_us, pending_ms, write_timestamp); + this_speaker->audio_output_callback_(audio_stream_info.bytes_to_frames(bytes_written), + now + dma_buffers_duration_ms * 1000); tx_dma_underflow = false; last_data_received_time = millis(); diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 121a62392c..8e480dd49b 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -53,14 +53,15 @@ void SourceSpeaker::dump_config() { } void SourceSpeaker::setup() { - this->parent_->get_output_speaker()->add_audio_output_callback( - [this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) { - uint32_t personal_playback_ms = std::min(new_playback_ms, this->pending_playback_ms_); - if (personal_playback_ms > 0) { - this->pending_playback_ms_ -= personal_playback_ms; - this->audio_output_callback_(personal_playback_ms, remainder_us, this->pending_playback_ms_, write_timestamp); - } - }); + this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) { + // The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames + uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_); + this->pending_playback_frames_ -= speakers_playback_frames; + + if (speakers_playback_frames > 0) { + this->audio_output_callback_(speakers_playback_frames, write_timestamp); + } + }); } void SourceSpeaker::loop() { @@ -153,6 +154,7 @@ esp_err_t SourceSpeaker::start_() { } } + this->pending_playback_frames_ = 0; // reset return this->parent_->start(this->audio_stream_info_); } @@ -542,11 +544,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { // Update source speaker buffer length transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); - speakers_with_data[0]->accumulated_frames_read_ += frames_to_mix; - - // Add new audio duration to the source speaker pending playback - speakers_with_data[0]->pending_playback_ms_ += - active_stream_info.frames_to_milliseconds_with_remainder(&speakers_with_data[0]->accumulated_frames_read_); + speakers_with_data[0]->pending_playback_frames_ += frames_to_mix; // Update output transfer buffer length output_transfer_buffer->increase_buffer_length( @@ -586,10 +584,6 @@ void MixerSpeaker::audio_mixer_task(void *params) { reinterpret_cast(output_transfer_buffer->get_buffer_end()), this_mixer->audio_stream_info_.value(), frames_to_mix); - speakers_with_data[i]->pending_playback_ms_ += - speakers_with_data[i]->get_audio_stream_info().frames_to_milliseconds_with_remainder( - &speakers_with_data[i]->accumulated_frames_read_); - if (i != transfer_buffers_with_data.size() - 1) { // Need to mix more streams together, point primary buffer and stream info to the already mixed output primary_buffer = reinterpret_cast(output_transfer_buffer->get_buffer_end()); @@ -601,11 +595,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { transfer_buffers_with_data[i]->decrease_buffer_length( speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); - speakers_with_data[i]->accumulated_frames_read_ += frames_to_mix; - - speakers_with_data[i]->pending_playback_ms_ += - speakers_with_data[i]->get_audio_stream_info().frames_to_milliseconds_with_remainder( - &speakers_with_data[i]->accumulated_frames_read_); + speakers_with_data[i]->pending_playback_frames_ += frames_to_mix; } // Update output transfer buffer length diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 0bd6b5f4c8..48bacc4471 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -114,9 +114,7 @@ class SourceSpeaker : public speaker::Speaker, public Component { uint32_t ducking_transition_samples_remaining_{0}; uint32_t samples_per_ducking_step_{0}; - uint32_t accumulated_frames_read_{0}; - - uint32_t pending_playback_ms_{0}; + uint32_t pending_playback_frames_{0}; }; class MixerSpeaker : public Component { diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 9bb46ad78c..5e5615cbb9 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -43,13 +43,18 @@ void ResamplerSpeaker::setup() { return; } - this->output_speaker_->add_audio_output_callback( - [this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) { - int32_t adjustment = this->playback_differential_ms_; - this->playback_differential_ms_ -= adjustment; - int32_t adjusted_playback_ms = static_cast(new_playback_ms) + adjustment; - this->audio_output_callback_(adjusted_playback_ms, remainder_us, pending_ms, write_timestamp); - }); + this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) { + if (this->audio_stream_info_.get_sample_rate() != this->target_stream_info_.get_sample_rate()) { + // Convert the number of frames from the target sample rate to the source sample rate. Track the remainder to + // avoid losing frames from integer division truncation. + const uint64_t numerator = new_frames * this->audio_stream_info_.get_sample_rate() + this->callback_remainder_; + const uint64_t denominator = this->target_stream_info_.get_sample_rate(); + this->callback_remainder_ = numerator % denominator; + this->audio_output_callback_(numerator / denominator, write_timestamp); + } else { + this->audio_output_callback_(new_frames, write_timestamp); + } + }); } void ResamplerSpeaker::loop() { @@ -283,7 +288,6 @@ void ResamplerSpeaker::resample_task(void *params) { xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); } - this_resampler->playback_differential_ms_ = 0; while (err == ESP_OK) { uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); @@ -295,8 +299,6 @@ void ResamplerSpeaker::resample_task(void *params) { int32_t ms_differential = 0; audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential); - this_resampler->playback_differential_ms_ += ms_differential; - if (resampler_state == audio::AudioResamplerState::FINISHED) { break; } else if (resampler_state == audio::AudioResamplerState::FAILED) { diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index d5e3f2b6d6..51790069d2 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -100,7 +100,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { uint32_t buffer_duration_ms_; - int32_t playback_differential_ms_{0}; + uint64_t callback_remainder_{0}; }; } // namespace resampler diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index e143920010..fed0207c93 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -106,16 +106,6 @@ void SpeakerMediaPlayer::setup() { ESP_LOGE(TAG, "Failed to create media pipeline"); this->mark_failed(); } - - // Setup callback to track the duration of audio played by the media pipeline - this->media_speaker_->add_audio_output_callback( - [this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) { - this->playback_ms_ += new_playback_ms; - this->remainder_us_ = remainder_us; - this->pending_ms_ = pending_ms; - this->last_audio_write_timestamp_ = write_timestamp; - this->playback_us_ = this->playback_ms_ * 1000 + this->remainder_us_; - }); } ESP_LOGI(TAG, "Set up speaker media player"); @@ -321,7 +311,6 @@ void SpeakerMediaPlayer::loop() { AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_; if (this->media_pipeline_ != nullptr) { this->media_pipeline_state_ = this->media_pipeline_->process_state(); - this->decoded_playback_ms_ = this->media_pipeline_->get_playback_ms(); } if (this->media_pipeline_state_ == AudioPipelineState::ERROR_READING) { @@ -379,13 +368,6 @@ void SpeakerMediaPlayer::loop() { } else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) { this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; } else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { - // Reset playback durations - this->decoded_playback_ms_ = 0; - this->playback_us_ = 0; - this->playback_ms_ = 0; - this->remainder_us_ = 0; - this->pending_ms_ = 0; - if (!media_playlist_.empty()) { uint32_t timeout_ms = 0; if (old_media_pipeline_state == AudioPipelineState::PLAYING) { diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 81eb72a830..67e9859a13 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -73,10 +73,6 @@ class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer { void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue); - uint32_t get_playback_ms() const { return this->playback_ms_; } - uint32_t get_playback_us() const { return this->playback_us_; } - uint32_t get_decoded_playback_ms() const { return this->decoded_playback_ms_; } - void set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms); protected: @@ -141,13 +137,6 @@ class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer { Trigger<> *mute_trigger_ = new Trigger<>(); Trigger<> *unmute_trigger_ = new Trigger<>(); Trigger *volume_trigger_ = new Trigger(); - - uint32_t decoded_playback_ms_{0}; - uint32_t playback_us_{0}; - uint32_t playback_ms_{0}; - uint32_t remainder_us_{0}; - uint32_t pending_ms_{0}; - uint32_t last_audio_write_timestamp_{0}; }; } // namespace speaker diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index c4cf912fa6..373d2e3a74 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -104,12 +104,9 @@ class Speaker { /// Callback function for sending the duration of the audio written to the speaker since the last callback. /// Parameters: - /// - Duration in milliseconds. Never rounded and should always be less than or equal to the actual duration. - /// - Remainder duration in microseconds. Rounded duration after subtracting the previous parameter from the actual - /// duration. - /// - Duration of remaining, unwritten audio buffered in the speaker in milliseconds. - /// - System time in microseconds when the last write was completed. - void add_audio_output_callback(std::function &&callback) { + /// - Frames played + /// - System time in microseconds when the frames were written to the DAC + void add_audio_output_callback(std::function &&callback) { this->audio_output_callback_.add(std::move(callback)); } @@ -123,7 +120,7 @@ class Speaker { audio_dac::AudioDac *audio_dac_{nullptr}; #endif - CallbackManager audio_output_callback_{}; + CallbackManager audio_output_callback_{}; }; } // namespace speaker From 8bdbde97324384cf20086994fcbf4e53c1b98045 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 07:50:13 +1200 Subject: [PATCH 006/193] Bump pylint from 3.3.6 to 3.3.7 (#8706) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 96568f2e3a..6478ab2017 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==3.3.6 +pylint==3.3.7 flake8==7.2.0 # also change in .pre-commit-config.yaml when updating ruff==0.11.7 # also change in .pre-commit-config.yaml when updating pyupgrade==3.19.1 # also change in .pre-commit-config.yaml when updating From 213648564c842f54b74a2909caaa8dc6af8aeb90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 10:19:23 +1200 Subject: [PATCH 007/193] Bump yamllint from 1.37.0 to 1.37.1 (#8705) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements_dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d11aa067bf..a63d5fc9c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/adrienverge/yamllint.git - rev: v1.35.1 + rev: v1.37.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-clang-format diff --git a/requirements_dev.txt b/requirements_dev.txt index d77ccaff69..16e051fcd7 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ # Useful stuff when working in a development environment clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating clang-tidy==18.1.8 # When updating clang-tidy, also update Dockerfile -yamllint==1.37.0 # also change in .pre-commit-config.yaml when updating +yamllint==1.37.1 # also change in .pre-commit-config.yaml when updating From d60e1f02c0e11848d37447f559d277ec244068ee Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 8 May 2025 08:22:56 +1000 Subject: [PATCH 008/193] [packet_transport] Make some arguments const (#8700) Co-authored-by: clydeps --- esphome/components/packet_transport/packet_transport.cpp | 2 +- esphome/components/packet_transport/packet_transport.h | 5 ++--- esphome/components/uart/packet_transport/uart_transport.cpp | 2 +- esphome/components/uart/packet_transport/uart_transport.h | 2 +- esphome/components/udp/packet_transport/udp_transport.cpp | 2 +- esphome/components/udp/packet_transport/udp_transport.h | 2 +- esphome/components/udp/udp_component.h | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 4514584408..be2f77e379 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -353,7 +353,7 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) { /** * Process a received packet */ -void PacketTransport::process_(std::vector &data) { +void PacketTransport::process_(const std::vector &data) { auto ping_key_seen = !this->ping_pong_enable_; PacketDecoder decoder((data.data()), data.size()); char namebuf[256]{}; diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 6799cb6ea1..34edb82963 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -101,12 +101,12 @@ class PacketTransport : public PollingComponent { protected: // child classes must implement this - virtual void send_packet(std::vector &buf) const = 0; + virtual void send_packet(const std::vector &buf) const = 0; virtual size_t get_max_packet_size() = 0; virtual bool should_send() { return true; } // to be called by child classes when a data packet is received. - void process_(std::vector &data); + void process_(const std::vector &data); void send_data_(bool all); void flush_(); void add_data_(uint8_t key, const char *id, float data); @@ -146,7 +146,6 @@ class PacketTransport : public PollingComponent { const char *platform_name_{""}; void add_key_(const char *name, uint32_t key); void send_ping_pong_request_(); - void process_ping_request_(const char *name, uint8_t *ptr, size_t len); inline bool is_encrypted_() { return !this->encryption_key_.empty(); } }; diff --git a/esphome/components/uart/packet_transport/uart_transport.cpp b/esphome/components/uart/packet_transport/uart_transport.cpp index aa11ae0772..423b657532 100644 --- a/esphome/components/uart/packet_transport/uart_transport.cpp +++ b/esphome/components/uart/packet_transport/uart_transport.cpp @@ -74,7 +74,7 @@ void UARTTransport::write_byte_(uint8_t byte) const { this->parent_->write_byte(byte); } -void UARTTransport::send_packet(std::vector &buf) const { +void UARTTransport::send_packet(const std::vector &buf) const { this->parent_->write_byte(FLAG_BYTE); for (uint8_t byte : buf) { this->write_byte_(byte); diff --git a/esphome/components/uart/packet_transport/uart_transport.h b/esphome/components/uart/packet_transport/uart_transport.h index db32859452..f1431e948c 100644 --- a/esphome/components/uart/packet_transport/uart_transport.h +++ b/esphome/components/uart/packet_transport/uart_transport.h @@ -29,7 +29,7 @@ class UARTTransport : public packet_transport::PacketTransport, public UARTDevic protected: void write_byte_(uint8_t byte) const; - void send_packet(std::vector &buf) const override; + void send_packet(const std::vector &buf) const override; bool should_send() override { return true; }; size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } std::vector receive_buffer_{}; diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp index 3918760627..b92e0d64df 100644 --- a/esphome/components/udp/packet_transport/udp_transport.cpp +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -31,6 +31,6 @@ void UDPTransport::update() { this->resend_data_ = this->should_broadcast_; } -void UDPTransport::send_packet(std::vector &buf) const { this->parent_->send_packet(buf); } +void UDPTransport::send_packet(const std::vector &buf) const { this->parent_->send_packet(buf); } } // namespace udp } // namespace esphome diff --git a/esphome/components/udp/packet_transport/udp_transport.h b/esphome/components/udp/packet_transport/udp_transport.h index 5a27bc32c7..4a95a095cc 100644 --- a/esphome/components/udp/packet_transport/udp_transport.h +++ b/esphome/components/udp/packet_transport/udp_transport.h @@ -16,7 +16,7 @@ class UDPTransport : public packet_transport::PacketTransport, public Parented &buf) const override; + void send_packet(const std::vector &buf) const override; bool should_send() override; bool should_broadcast_{false}; size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index 25909eba1d..065789ae28 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -30,7 +30,7 @@ class UDPComponent : public Component { void loop() override; void dump_config() override; void send_packet(const uint8_t *data, size_t size); - void send_packet(std::vector &buf) { this->send_packet(buf.data(), buf.size()); } + void send_packet(const std::vector &buf) { this->send_packet(buf.data(), buf.size()); } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }; protected: From 54ead9a6b47a040da58034c406425395f8002357 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 May 2025 21:56:54 -0500 Subject: [PATCH 009/193] Reserve buffer space to avoid frequent realloc when generating protobuf messages (#8707) --- esphome/components/api/api_pb2.cpp | 984 ++++++++++++++++++++- esphome/components/api/api_pb2.h | 139 ++- esphome/components/api/api_pb2_service.cpp | 2 +- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/api_pb2_size.h | 361 ++++++++ esphome/components/api/proto.h | 11 + script/api_protobuf/api_protobuf.py | 213 ++++- 7 files changed, 1705 insertions(+), 7 deletions(-) create mode 100644 esphome/components/api/api_pb2_size.h diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 90e5bcb548..b5d8bb3d79 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1,6 +1,7 @@ // This file was automatically generated with a tool. -// See scripts/api_protobuf/api_protobuf.py +// See script/api_protobuf/api_protobuf.py #include "api_pb2.h" +#include "api_pb2_size.h" #include "esphome/core/log.h" #include @@ -654,6 +655,11 @@ void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->api_version_major); buffer.encode_uint32(3, this->api_version_minor); } +void HelloRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->client_info, false); + ProtoSize::add_uint32_field(total_size, 1, this->api_version_major, false); + ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void HelloRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -708,6 +714,12 @@ void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, this->server_info); buffer.encode_string(4, this->name); } +void HelloResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->api_version_major, false); + ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor, false); + ProtoSize::add_string_field(total_size, 1, this->server_info, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void HelloResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -743,6 +755,9 @@ bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value } } void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); } +void ConnectRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->password, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ConnectRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -764,6 +779,9 @@ bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { } } void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } +void ConnectResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->invalid_password, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ConnectResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -775,22 +793,27 @@ void ConnectResponse::dump_to(std::string &out) const { } #endif void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} +void DisconnectRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } #endif void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} +void DisconnectResponse::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } #endif void PingRequest::encode(ProtoWriteBuffer buffer) const {} +void PingRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } #endif void PingResponse::encode(ProtoWriteBuffer buffer) const {} +void PingResponse::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } #endif void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} +void DeviceInfoRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } #endif @@ -903,6 +926,27 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(18, this->bluetooth_mac_address); buffer.encode_bool(19, this->api_encryption_supported); } +void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->uses_password, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->mac_address, false); + ProtoSize::add_string_field(total_size, 1, this->esphome_version, false); + ProtoSize::add_string_field(total_size, 1, this->compilation_time, false); + ProtoSize::add_string_field(total_size, 1, this->model, false); + ProtoSize::add_bool_field(total_size, 1, this->has_deep_sleep, false); + ProtoSize::add_string_field(total_size, 1, this->project_name, false); + ProtoSize::add_string_field(total_size, 1, this->project_version, false); + ProtoSize::add_uint32_field(total_size, 1, this->webserver_port, false); + ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version, false); + ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags, false); + ProtoSize::add_string_field(total_size, 1, this->manufacturer, false); + ProtoSize::add_string_field(total_size, 1, this->friendly_name, false); + ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version, false); + ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags, false); + ProtoSize::add_string_field(total_size, 2, this->suggested_area, false); + ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false); + ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -991,14 +1035,17 @@ void DeviceInfoResponse::dump_to(std::string &out) const { } #endif void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} +void ListEntitiesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } #endif void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} +void ListEntitiesDoneResponse::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } #endif void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeStatesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } #endif @@ -1067,6 +1114,17 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->icon); buffer.encode_enum(9, this->entity_category); } +void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_bool_field(total_size, 1, this->is_status_binary_sensor, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1139,6 +1197,11 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); } +void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BinarySensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1238,6 +1301,20 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(11, this->entity_category); buffer.encode_bool(12, this->supports_stop); } +void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_tilt, false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1332,6 +1409,13 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(4, this->tilt); buffer.encode_enum(5, this->current_operation); } +void CoverStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_state), false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void CoverStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1415,6 +1499,16 @@ void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->tilt); buffer.encode_bool(8, this->stop); } +void CoverCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_legacy_command, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_command), false); + ProtoSize::add_bool_field(total_size, 1, this->has_position, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_tilt, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->stop, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void CoverCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1538,6 +1632,24 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(12, it, true); } } +void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_speed, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_direction, false); + ProtoSize::add_int32_field(total_size, 1, this->supported_speed_count, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + if (!this->supported_preset_modes.empty()) { + for (const auto &it : this->supported_preset_modes) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1651,6 +1763,15 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_int32(6, this->speed_level); buffer.encode_string(7, this->preset_mode); } +void FanStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->oscillating, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed), false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction), false); + ProtoSize::add_int32_field(total_size, 1, this->speed_level, false); + ProtoSize::add_string_field(total_size, 1, this->preset_mode, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1772,6 +1893,21 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(12, this->has_preset_mode); buffer.encode_string(13, this->preset_mode); } +void FanCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_state, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->has_speed, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed), false); + ProtoSize::add_bool_field(total_size, 1, this->has_oscillating, false); + ProtoSize::add_bool_field(total_size, 1, this->oscillating, false); + ProtoSize::add_bool_field(total_size, 1, this->has_direction, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction), false); + ProtoSize::add_bool_field(total_size, 1, this->has_speed_level, false); + ProtoSize::add_int32_field(total_size, 1, this->speed_level, false); + ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false); + ProtoSize::add_string_field(total_size, 1, this->preset_mode, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1931,6 +2067,31 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(14, this->icon); buffer.encode_enum(15, this->entity_category); } +void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + if (!this->supported_color_modes.empty()) { + for (const auto &it : this->supported_color_modes) { + ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + } + } + ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_brightness, false); + ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb, false); + ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value, false); + ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->min_mireds != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->max_mireds != 0.0f, false); + if (!this->effects.empty()) { + for (const auto &it : this->effects) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2089,6 +2250,21 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); } +void LightStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->color_mode), false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->color_brightness != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f, false); + ProtoSize::add_string_field(total_size, 1, this->effect, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void LightStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2312,6 +2488,35 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(18, this->has_effect); buffer.encode_string(19, this->effect); } +void LightCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_state, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->has_brightness, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f, false); + ProtoSize::add_bool_field(total_size, 2, this->has_color_mode, false); + ProtoSize::add_enum_field(total_size, 2, static_cast(this->color_mode), false); + ProtoSize::add_bool_field(total_size, 2, this->has_color_brightness, false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->color_brightness != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_rgb, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_white, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_color_temperature, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f, false); + ProtoSize::add_bool_field(total_size, 2, this->has_cold_white, false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->cold_white != 0.0f, false); + ProtoSize::add_bool_field(total_size, 2, this->has_warm_white, false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->warm_white != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_transition_length, false); + ProtoSize::add_uint32_field(total_size, 1, this->transition_length, false); + ProtoSize::add_bool_field(total_size, 2, this->has_flash_length, false); + ProtoSize::add_uint32_field(total_size, 2, this->flash_length, false); + ProtoSize::add_bool_field(total_size, 2, this->has_effect, false); + ProtoSize::add_string_field(total_size, 2, this->effect, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void LightCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2523,6 +2728,21 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(12, this->disabled_by_default); buffer.encode_enum(13, this->entity_category); } +void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); + ProtoSize::add_int32_field(total_size, 1, this->accuracy_decimals, false); + ProtoSize::add_bool_field(total_size, 1, this->force_update, false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state_class), false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type), false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2612,6 +2832,11 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); } +void SensorStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2697,6 +2922,17 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(8, this->entity_category); buffer.encode_string(9, this->device_class); } +void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2764,6 +3000,10 @@ void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); } +void SwitchStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SwitchStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2803,6 +3043,10 @@ void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); } +void SwitchCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SwitchCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2878,6 +3122,16 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); } +void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2952,6 +3206,11 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); } +void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void TextSensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -2989,6 +3248,10 @@ void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->level); buffer.encode_bool(2, this->dump_config); } +void SubscribeLogsRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_enum_field(total_size, 1, static_cast(this->level), false); + ProtoSize::add_bool_field(total_size, 1, this->dump_config, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeLogsRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3032,6 +3295,11 @@ void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, this->message); buffer.encode_bool(4, this->send_failed); } +void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_enum_field(total_size, 1, static_cast(this->level), false); + ProtoSize::add_string_field(total_size, 1, this->message, false); + ProtoSize::add_bool_field(total_size, 1, this->send_failed, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeLogsResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3061,6 +3329,9 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } } void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); } +void NoiseEncryptionSetKeyRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->key, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3082,6 +3353,9 @@ bool NoiseEncryptionSetKeyResponse::decode_varint(uint32_t field_id, ProtoVarInt } } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->success, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3093,6 +3367,7 @@ void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { } #endif void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeHomeassistantServicesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeassistantServicesRequest {}"); @@ -3116,6 +3391,10 @@ void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } +void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->key, false); + ProtoSize::add_string_field(total_size, 1, this->value, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void HomeassistantServiceMap::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3175,6 +3454,13 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(5, this->is_event); } +void HomeassistantServiceResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->service, false); + ProtoSize::add_repeated_message(total_size, 1, this->data); + ProtoSize::add_repeated_message(total_size, 1, this->data_template); + ProtoSize::add_repeated_message(total_size, 1, this->variables); + ProtoSize::add_bool_field(total_size, 1, this->is_event, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void HomeassistantServiceResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3208,6 +3494,7 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { } #endif void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeHomeAssistantStatesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); @@ -3242,6 +3529,11 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); } +void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->entity_id, false); + ProtoSize::add_string_field(total_size, 1, this->attribute, false); + ProtoSize::add_bool_field(total_size, 1, this->once, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3283,6 +3575,11 @@ void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_string(3, this->attribute); } +void HomeAssistantStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->entity_id, false); + ProtoSize::add_string_field(total_size, 1, this->state, false); + ProtoSize::add_string_field(total_size, 1, this->attribute, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void HomeAssistantStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3302,6 +3599,7 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { } #endif void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} +void GetTimeRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } #endif @@ -3316,6 +3614,9 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } } void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } +void GetTimeResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3351,6 +3652,10 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); buffer.encode_enum(2, this->type); } +void ListEntitiesServicesArgument::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->type), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesServicesArgument::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3396,6 +3701,11 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(3, it, true); } } +void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_repeated_message(total_size, 1, this->args); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesServicesResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3490,6 +3800,33 @@ void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(9, it, true); } } +void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->bool_, false); + ProtoSize::add_int32_field(total_size, 1, this->legacy_int, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->float_ != 0.0f, false); + ProtoSize::add_string_field(total_size, 1, this->string_, false); + ProtoSize::add_sint32_field(total_size, 1, this->int_, false); + if (!this->bool_array.empty()) { + for (const auto it : this->bool_array) { + ProtoSize::add_bool_field(total_size, 1, it, true); + } + } + if (!this->int_array.empty()) { + for (const auto &it : this->int_array) { + ProtoSize::add_sint32_field(total_size, 1, it, true); + } + } + if (!this->float_array.empty()) { + for (const auto &it : this->float_array) { + ProtoSize::add_fixed_field<4>(total_size, 1, it != 0.0f, true); + } + } + if (!this->string_array.empty()) { + for (const auto &it : this->string_array) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } +} #ifdef HAS_PROTO_MESSAGE_DUMP void ExecuteServiceArgument::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3571,6 +3908,10 @@ void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(2, it, true); } } +void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_repeated_message(total_size, 1, this->args); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ExecuteServiceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3643,6 +3984,15 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(6, this->icon); buffer.encode_enum(7, this->entity_category); } +void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3713,6 +4063,11 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->data); buffer.encode_bool(3, this->done); } +void CameraImageResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->data, false); + ProtoSize::add_bool_field(total_size, 1, this->done, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void CameraImageResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3750,6 +4105,10 @@ void CameraImageRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->single); buffer.encode_bool(2, this->stream); } +void CameraImageRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->single, false); + ProtoSize::add_bool_field(total_size, 1, this->stream, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void CameraImageRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -3921,6 +4280,57 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(24, this->visual_min_humidity); buffer.encode_float(25, this->visual_max_humidity); } +void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_two_point_target_temperature, false); + if (!this->supported_modes.empty()) { + for (const auto &it : this->supported_modes) { + ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + } + } + ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_min_temperature != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_max_temperature != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_target_temperature_step != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_action, false); + if (!this->supported_fan_modes.empty()) { + for (const auto &it : this->supported_fan_modes) { + ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + } + } + if (!this->supported_swing_modes.empty()) { + for (const auto &it : this->supported_swing_modes) { + ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + } + } + if (!this->supported_custom_fan_modes.empty()) { + for (const auto &it : this->supported_custom_fan_modes) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } + if (!this->supported_presets.empty()) { + for (const auto &it : this->supported_presets) { + ProtoSize::add_enum_field(total_size, 2, static_cast(it), true); + } + } + if (!this->supported_custom_presets.empty()) { + for (const auto &it : this->supported_custom_presets) { + ProtoSize::add_string_field(total_size, 2, it, true); + } + } + ProtoSize::add_bool_field(total_size, 2, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 2, this->icon, false); + ProtoSize::add_enum_field(total_size, 2, static_cast(this->entity_category), false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_current_temperature_step != 0.0f, false); + ProtoSize::add_bool_field(total_size, 2, this->supports_current_humidity, false); + ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4141,6 +4551,23 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(14, this->current_humidity); buffer.encode_float(15, this->target_humidity); } +void ClimateStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->current_temperature != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->action), false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode), false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode), false); + ProtoSize::add_string_field(total_size, 1, this->custom_fan_mode, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->preset), false); + ProtoSize::add_string_field(total_size, 1, this->custom_preset, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4349,6 +4776,31 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(22, this->has_target_humidity); buffer.encode_float(23, this->target_humidity); } +void ClimateCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_mode, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_bool_field(total_size, 1, this->has_target_temperature, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_target_temperature_low, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_target_temperature_high, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->unused_has_legacy_away, false); + ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away, false); + ProtoSize::add_bool_field(total_size, 1, this->has_fan_mode, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode), false); + ProtoSize::add_bool_field(total_size, 1, this->has_swing_mode, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode), false); + ProtoSize::add_bool_field(total_size, 2, this->has_custom_fan_mode, false); + ProtoSize::add_string_field(total_size, 2, this->custom_fan_mode, false); + ProtoSize::add_bool_field(total_size, 2, this->has_preset, false); + ProtoSize::add_enum_field(total_size, 2, static_cast(this->preset), false); + ProtoSize::add_bool_field(total_size, 2, this->has_custom_preset, false); + ProtoSize::add_string_field(total_size, 2, this->custom_preset, false); + ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false); + ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4537,6 +4989,21 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(12, this->mode); buffer.encode_string(13, this->device_class); } +void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->min_value != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->max_value != 0.0f, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->step != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesNumberResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4628,6 +5095,11 @@ void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); } +void NumberStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void NumberStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4666,6 +5138,10 @@ void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); } +void NumberCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void NumberCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4744,6 +5220,20 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); } +void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + if (!this->options.empty()) { + for (const auto &it : this->options) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSelectResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4820,6 +5310,11 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); } +void SelectStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SelectStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4863,6 +5358,10 @@ void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); } +void SelectCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SelectCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -4953,6 +5452,19 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(10, this->requires_code); buffer.encode_string(11, this->code_format); } +void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_open, false); + ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); + ProtoSize::add_string_field(total_size, 1, this->code_format, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLockResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5028,6 +5540,10 @@ void LockStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_enum(2, this->state); } +void LockStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void LockStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5083,6 +5599,12 @@ void LockCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->has_code); buffer.encode_string(4, this->code); } +void LockCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); + ProtoSize::add_bool_field(total_size, 1, this->has_code, false); + ProtoSize::add_string_field(total_size, 1, this->code, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void LockCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5166,6 +5688,16 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); } +void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesButtonResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5216,6 +5748,9 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } } void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); } +void ButtonCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ButtonCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5266,6 +5801,13 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(4, this->purpose); buffer.encode_uint32(5, this->sample_bytes); } +void MediaPlayerSupportedFormat::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->format, false); + ProtoSize::add_uint32_field(total_size, 1, this->sample_rate, false); + ProtoSize::add_uint32_field(total_size, 1, this->num_channels, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->purpose), false); + ProtoSize::add_uint32_field(total_size, 1, this->sample_bytes, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void MediaPlayerSupportedFormat::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5362,6 +5904,17 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(9, it, true); } } +void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false); + ProtoSize::add_repeated_message(total_size, 1, this->supported_formats); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5441,6 +5994,12 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); } +void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->muted, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void MediaPlayerStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5530,6 +6089,17 @@ void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->has_announcement); buffer.encode_bool(9, this->announcement); } +void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_command, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); + ProtoSize::add_bool_field(total_size, 1, this->has_volume, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->has_media_url, false); + ProtoSize::add_string_field(total_size, 1, this->media_url, false); + ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false); + ProtoSize::add_bool_field(total_size, 1, this->announcement, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void MediaPlayerCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5587,6 +6157,9 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->flags); } +void SubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->flags, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5629,6 +6202,15 @@ void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { } buffer.encode_string(3, this->data); } +void BluetoothServiceData::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->uuid, false); + if (!this->legacy_data.empty()) { + for (const auto &it : this->legacy_data) { + ProtoSize::add_uint32_field(total_size, 1, it, true); + } + } + ProtoSize::add_string_field(total_size, 1, this->data, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothServiceData::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5705,6 +6287,19 @@ void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_uint32(7, this->address_type); } +void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_sint32_field(total_size, 1, this->rssi, false); + if (!this->service_uuids.empty()) { + for (const auto &it : this->service_uuids) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } + ProtoSize::add_repeated_message(total_size, 1, this->service_data); + ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data); + ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5782,6 +6377,12 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->address_type); buffer.encode_string(4, this->data); } +void BluetoothLERawAdvertisement::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_sint32_field(total_size, 1, this->rssi, false); + ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); + ProtoSize::add_string_field(total_size, 1, this->data, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothLERawAdvertisement::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5822,6 +6423,9 @@ void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const buffer.encode_message(1, it, true); } } +void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_repeated_message(total_size, 1, this->advertisements); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5862,6 +6466,12 @@ void BluetoothDeviceRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->has_address_type); buffer.encode_uint32(4, this->address_type); } +void BluetoothDeviceRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->request_type), false); + ProtoSize::add_bool_field(total_size, 1, this->has_address_type, false); + ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothDeviceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5914,6 +6524,12 @@ void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->mtu); buffer.encode_int32(4, this->error); } +void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_bool_field(total_size, 1, this->connected, false); + ProtoSize::add_uint32_field(total_size, 1, this->mtu, false); + ProtoSize::add_int32_field(total_size, 1, this->error, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5950,6 +6566,9 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI } } void BluetoothGATTGetServicesRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); } +void BluetoothGATTGetServicesRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -5981,6 +6600,14 @@ void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { } buffer.encode_uint32(2, this->handle); } +void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { + if (!this->uuid.empty()) { + for (const auto &it : this->uuid) { + ProtoSize::add_uint64_field(total_size, 1, it, true); + } + } + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTDescriptor::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6037,6 +6664,16 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(4, it, true); } } +void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { + if (!this->uuid.empty()) { + for (const auto &it : this->uuid) { + ProtoSize::add_uint64_field(total_size, 1, it, true); + } + } + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_uint32_field(total_size, 1, this->properties, false); + ProtoSize::add_repeated_message(total_size, 1, this->descriptors); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTCharacteristic::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6099,6 +6736,15 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(3, it, true); } } +void BluetoothGATTService::calculate_size(uint32_t &total_size) const { + if (!this->uuid.empty()) { + for (const auto &it : this->uuid) { + ProtoSize::add_uint64_field(total_size, 1, it, true); + } + } + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_repeated_message(total_size, 1, this->characteristics); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTService::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6149,6 +6795,10 @@ void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(2, it, true); } } +void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_repeated_message(total_size, 1, this->services); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6179,6 +6829,9 @@ bool BluetoothGATTGetServicesDoneResponse::decode_varint(uint32_t field_id, Prot void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); } +void BluetoothGATTGetServicesDoneResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6208,6 +6861,10 @@ void BluetoothGATTReadRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } +void BluetoothGATTReadRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTReadRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6253,6 +6910,11 @@ void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_string(3, this->data); } +void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_string_field(total_size, 1, this->data, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTReadResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6307,6 +6969,12 @@ void BluetoothGATTWriteRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->response); buffer.encode_string(4, this->data); } +void BluetoothGATTWriteRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_bool_field(total_size, 1, this->response, false); + ProtoSize::add_string_field(total_size, 1, this->data, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTWriteRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6349,6 +7017,10 @@ void BluetoothGATTReadDescriptorRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } +void BluetoothGATTReadDescriptorRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6394,6 +7066,11 @@ void BluetoothGATTWriteDescriptorRequest::encode(ProtoWriteBuffer buffer) const buffer.encode_uint32(2, this->handle); buffer.encode_string(3, this->data); } +void BluetoothGATTWriteDescriptorRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_string_field(total_size, 1, this->data, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6437,6 +7114,11 @@ void BluetoothGATTNotifyRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bool(3, this->enable); } +void BluetoothGATTNotifyRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_bool_field(total_size, 1, this->enable, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6486,6 +7168,11 @@ void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_string(3, this->data); } +void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_string_field(total_size, 1, this->data, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6507,6 +7194,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { } #endif void SubscribeBluetoothConnectionsFreeRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeBluetoothConnectionsFreeRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { out.append("SubscribeBluetoothConnectionsFreeRequest {}"); @@ -6537,6 +7225,15 @@ void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(3, it, true); } } +void BluetoothConnectionsFreeResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->free, false); + ProtoSize::add_uint32_field(total_size, 1, this->limit, false); + if (!this->allocated.empty()) { + for (const auto &it : this->allocated) { + ProtoSize::add_uint64_field(total_size, 1, it, true); + } + } +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6583,6 +7280,11 @@ void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); } +void BluetoothGATTErrorResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_int32_field(total_size, 1, this->error, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTErrorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6622,6 +7324,10 @@ void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } +void BluetoothGATTWriteResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTWriteResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6656,6 +7362,10 @@ void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } +void BluetoothGATTNotifyResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6695,6 +7405,11 @@ void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); } +void BluetoothDevicePairingResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_bool_field(total_size, 1, this->paired, false); + ProtoSize::add_int32_field(total_size, 1, this->error, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothDevicePairingResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6738,6 +7453,11 @@ void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } +void BluetoothDeviceUnpairingResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_bool_field(total_size, 1, this->success, false); + ProtoSize::add_int32_field(total_size, 1, this->error, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6759,6 +7479,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { } #endif void UnsubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} +void UnsubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); @@ -6787,6 +7508,11 @@ void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } +void BluetoothDeviceClearCacheResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_bool_field(total_size, 1, this->success, false); + ProtoSize::add_int32_field(total_size, 1, this->error, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6825,6 +7551,10 @@ void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->state); buffer.encode_enum(2, this->mode); } +void BluetoothScannerStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothScannerStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6852,6 +7582,9 @@ bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarIn void BluetoothScannerSetModeRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->mode); } +void BluetoothScannerSetModeRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6880,6 +7613,10 @@ void SubscribeVoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->subscribe); buffer.encode_uint32(2, this->flags); } +void SubscribeVoiceAssistantRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->subscribe, false); + ProtoSize::add_uint32_field(total_size, 1, this->flags, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6924,6 +7661,11 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); } +void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->noise_suppression_level, false); + ProtoSize::add_uint32_field(total_size, 1, this->auto_gain, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantAudioSettings::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6984,6 +7726,13 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } +void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->start, false); + ProtoSize::add_string_field(total_size, 1, this->conversation_id, false); + ProtoSize::add_uint32_field(total_size, 1, this->flags, false); + ProtoSize::add_message_object(total_size, 1, this->audio_settings, false); + ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7029,6 +7778,10 @@ void VoiceAssistantResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->port); buffer.encode_bool(2, this->error); } +void VoiceAssistantResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->port, false); + ProtoSize::add_bool_field(total_size, 1, this->error, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7062,6 +7815,10 @@ void VoiceAssistantEventData::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); buffer.encode_string(2, this->value); } +void VoiceAssistantEventData::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->value, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantEventData::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7102,6 +7859,10 @@ void VoiceAssistantEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(2, it, true); } } +void VoiceAssistantEventResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_enum_field(total_size, 1, static_cast(this->event_type), false); + ProtoSize::add_repeated_message(total_size, 1, this->data); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantEventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7142,6 +7903,10 @@ void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->data); buffer.encode_bool(2, this->end); } +void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->data, false); + ProtoSize::add_bool_field(total_size, 1, this->end, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantAudio::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7200,6 +7965,14 @@ void VoiceAssistantTimerEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(5, this->seconds_left); buffer.encode_bool(6, this->is_active); } +void VoiceAssistantTimerEventResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_enum_field(total_size, 1, static_cast(this->event_type), false); + ProtoSize::add_string_field(total_size, 1, this->timer_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_uint32_field(total_size, 1, this->total_seconds, false); + ProtoSize::add_uint32_field(total_size, 1, this->seconds_left, false); + ProtoSize::add_bool_field(total_size, 1, this->is_active, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7266,6 +8039,12 @@ void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, this->preannounce_media_id); buffer.encode_bool(4, this->start_conversation); } +void VoiceAssistantAnnounceRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->media_id, false); + ProtoSize::add_string_field(total_size, 1, this->text, false); + ProtoSize::add_string_field(total_size, 1, this->preannounce_media_id, false); + ProtoSize::add_bool_field(total_size, 1, this->start_conversation, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7299,6 +8078,9 @@ bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarIn } } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const { + ProtoSize::add_bool_field(total_size, 1, this->success, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7334,6 +8116,15 @@ void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, it, true); } } +void VoiceAssistantWakeWord::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->id, false); + ProtoSize::add_string_field(total_size, 1, this->wake_word, false); + if (!this->trained_languages.empty()) { + for (const auto &it : this->trained_languages) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantWakeWord::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7355,6 +8146,7 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { } #endif void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} +void VoiceAssistantConfigurationRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { out.append("VoiceAssistantConfigurationRequest {}"); @@ -7393,6 +8185,15 @@ void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const } buffer.encode_uint32(3, this->max_active_wake_words); } +void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_repeated_message(total_size, 1, this->available_wake_words); + if (!this->active_wake_words.empty()) { + for (const auto &it : this->active_wake_words) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } + ProtoSize::add_uint32_field(total_size, 1, this->max_active_wake_words, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7431,6 +8232,13 @@ void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, it, true); } } +void VoiceAssistantSetConfiguration::calculate_size(uint32_t &total_size) const { + if (!this->active_wake_words.empty()) { + for (const auto &it : this->active_wake_words) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7513,6 +8321,18 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_bool(9, this->requires_code); buffer.encode_bool(10, this->requires_code_to_arm); } +void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false); + ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); + ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7585,6 +8405,10 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_enum(2, this->state); } +void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void AlarmControlPanelStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7635,6 +8459,11 @@ void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(2, this->command); buffer.encode_string(3, this->code); } +void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); + ProtoSize::add_string_field(total_size, 1, this->code, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7729,6 +8558,19 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->pattern); buffer.encode_enum(11, this->mode); } +void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->min_length, false); + ProtoSize::add_uint32_field(total_size, 1, this->max_length, false); + ProtoSize::add_string_field(total_size, 1, this->pattern, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7817,6 +8659,11 @@ void TextStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); } +void TextStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void TextStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7860,6 +8707,10 @@ void TextCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); } +void TextCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->state, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void TextCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -7930,6 +8781,15 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); } +void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8004,6 +8864,13 @@ void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->month); buffer.encode_uint32(5, this->day); } +void DateStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_uint32_field(total_size, 1, this->year, false); + ProtoSize::add_uint32_field(total_size, 1, this->month, false); + ProtoSize::add_uint32_field(total_size, 1, this->day, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void DateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8068,6 +8935,12 @@ void DateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->month); buffer.encode_uint32(4, this->day); } +void DateCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_uint32_field(total_size, 1, this->year, false); + ProtoSize::add_uint32_field(total_size, 1, this->month, false); + ProtoSize::add_uint32_field(total_size, 1, this->day, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void DateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8149,6 +9022,15 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); } +void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8223,6 +9105,13 @@ void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->minute); buffer.encode_uint32(5, this->second); } +void TimeStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_uint32_field(total_size, 1, this->hour, false); + ProtoSize::add_uint32_field(total_size, 1, this->minute, false); + ProtoSize::add_uint32_field(total_size, 1, this->second, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void TimeStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8287,6 +9176,12 @@ void TimeCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->minute); buffer.encode_uint32(4, this->second); } +void TimeCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_uint32_field(total_size, 1, this->hour, false); + ProtoSize::add_uint32_field(total_size, 1, this->minute, false); + ProtoSize::add_uint32_field(total_size, 1, this->second, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void TimeCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8380,6 +9275,21 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(9, it, true); } } +void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); + if (!this->event_types.empty()) { + for (const auto &it : this->event_types) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesEventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8449,6 +9359,10 @@ void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); } +void EventResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->event_type, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void EventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8539,6 +9453,19 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); } +void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesValveResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8619,6 +9546,11 @@ void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(2, this->position); buffer.encode_enum(3, this->current_operation); } +void ValveStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ValveStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8673,6 +9605,12 @@ void ValveCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(3, this->position); buffer.encode_bool(4, this->stop); } +void ValveCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_position, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); + ProtoSize::add_bool_field(total_size, 1, this->stop, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ValveCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8752,6 +9690,15 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); } +void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8816,6 +9763,11 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); } +void DateTimeStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void DateTimeStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8854,6 +9806,10 @@ void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_fixed32(2, this->epoch_seconds); } +void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void DateTimeCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -8930,6 +9886,16 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); } +void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->device_class, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesUpdateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -9039,6 +10005,18 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(9, this->release_summary); buffer.encode_string(10, this->release_url); } +void UpdateStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_bool_field(total_size, 1, this->in_progress, false); + ProtoSize::add_bool_field(total_size, 1, this->has_progress, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->progress != 0.0f, false); + ProtoSize::add_string_field(total_size, 1, this->current_version, false); + ProtoSize::add_string_field(total_size, 1, this->latest_version, false); + ProtoSize::add_string_field(total_size, 1, this->title, false); + ProtoSize::add_string_field(total_size, 1, this->release_summary, false); + ProtoSize::add_string_field(total_size, 1, this->release_url, false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void UpdateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -9111,6 +10089,10 @@ void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_enum(2, this->command); } +void UpdateCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); +} #ifdef HAS_PROTO_MESSAGE_DUMP void UpdateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 18e4002107..457797f1a7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1,8 +1,9 @@ // This file was automatically generated with a tool. -// See scripts/api_protobuf/api_protobuf.py +// See script/api_protobuf/api_protobuf.py #pragma once #include "proto.h" +#include "api_pb2_size.h" namespace esphome { namespace api { @@ -257,6 +258,7 @@ class HelloRequest : public ProtoMessage { uint32_t api_version_major{0}; uint32_t api_version_minor{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -272,6 +274,7 @@ class HelloResponse : public ProtoMessage { std::string server_info{}; std::string name{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -284,6 +287,7 @@ class ConnectRequest : public ProtoMessage { public: std::string password{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -295,6 +299,7 @@ class ConnectResponse : public ProtoMessage { public: bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -305,6 +310,7 @@ class ConnectResponse : public ProtoMessage { class DisconnectRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -314,6 +320,7 @@ class DisconnectRequest : public ProtoMessage { class DisconnectResponse : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -323,6 +330,7 @@ class DisconnectResponse : public ProtoMessage { class PingRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -332,6 +340,7 @@ class PingRequest : public ProtoMessage { class PingResponse : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -341,6 +350,7 @@ class PingResponse : public ProtoMessage { class DeviceInfoRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -369,6 +379,7 @@ class DeviceInfoResponse : public ProtoMessage { std::string bluetooth_mac_address{}; bool api_encryption_supported{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -380,6 +391,7 @@ class DeviceInfoResponse : public ProtoMessage { class ListEntitiesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -389,6 +401,7 @@ class ListEntitiesRequest : public ProtoMessage { class ListEntitiesDoneResponse : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -398,6 +411,7 @@ class ListEntitiesDoneResponse : public ProtoMessage { class SubscribeStatesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -416,6 +430,7 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -431,6 +446,7 @@ class BinarySensorStateResponse : public ProtoMessage { bool state{false}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -454,6 +470,7 @@ class ListEntitiesCoverResponse : public ProtoMessage { enums::EntityCategory entity_category{}; bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -471,6 +488,7 @@ class CoverStateResponse : public ProtoMessage { float tilt{0.0f}; enums::CoverOperation current_operation{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -490,6 +508,7 @@ class CoverCommandRequest : public ProtoMessage { float tilt{0.0f}; bool stop{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -513,6 +532,7 @@ class ListEntitiesFanResponse : public ProtoMessage { enums::EntityCategory entity_category{}; std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -532,6 +552,7 @@ class FanStateResponse : public ProtoMessage { int32_t speed_level{0}; std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -557,6 +578,7 @@ class FanCommandRequest : public ProtoMessage { bool has_preset_mode{false}; std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -584,6 +606,7 @@ class ListEntitiesLightResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -609,6 +632,7 @@ class LightStateResponse : public ProtoMessage { float warm_white{0.0f}; std::string effect{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -648,6 +672,7 @@ class LightCommandRequest : public ProtoMessage { bool has_effect{false}; std::string effect{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -673,6 +698,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -688,6 +714,7 @@ class SensorStateResponse : public ProtoMessage { float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -708,6 +735,7 @@ class ListEntitiesSwitchResponse : public ProtoMessage { enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -722,6 +750,7 @@ class SwitchStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -735,6 +764,7 @@ class SwitchCommandRequest : public ProtoMessage { uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -754,6 +784,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -769,6 +800,7 @@ class TextSensorStateResponse : public ProtoMessage { std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -783,6 +815,7 @@ class SubscribeLogsRequest : public ProtoMessage { enums::LogLevel level{}; bool dump_config{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -796,6 +829,7 @@ class SubscribeLogsResponse : public ProtoMessage { std::string message{}; bool send_failed{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -808,6 +842,7 @@ class NoiseEncryptionSetKeyRequest : public ProtoMessage { public: std::string key{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -819,6 +854,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { public: bool success{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -829,6 +865,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { class SubscribeHomeassistantServicesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -840,6 +877,7 @@ class HomeassistantServiceMap : public ProtoMessage { std::string key{}; std::string value{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -855,6 +893,7 @@ class HomeassistantServiceResponse : public ProtoMessage { std::vector variables{}; bool is_event{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -866,6 +905,7 @@ class HomeassistantServiceResponse : public ProtoMessage { class SubscribeHomeAssistantStatesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -878,6 +918,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { std::string attribute{}; bool once{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -892,6 +933,7 @@ class HomeAssistantStateResponse : public ProtoMessage { std::string state{}; std::string attribute{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -902,6 +944,7 @@ class HomeAssistantStateResponse : public ProtoMessage { class GetTimeRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -912,6 +955,7 @@ class GetTimeResponse : public ProtoMessage { public: uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -924,6 +968,7 @@ class ListEntitiesServicesArgument : public ProtoMessage { std::string name{}; enums::ServiceArgType type{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -938,6 +983,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { uint32_t key{0}; std::vector args{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -958,6 +1004,7 @@ class ExecuteServiceArgument : public ProtoMessage { std::vector float_array{}; std::vector string_array{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -972,6 +1019,7 @@ class ExecuteServiceRequest : public ProtoMessage { uint32_t key{0}; std::vector args{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -990,6 +1038,7 @@ class ListEntitiesCameraResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1005,6 +1054,7 @@ class CameraImageResponse : public ProtoMessage { std::string data{}; bool done{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1019,6 +1069,7 @@ class CameraImageRequest : public ProtoMessage { bool single{false}; bool stream{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1054,6 +1105,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1081,6 +1133,7 @@ class ClimateStateResponse : public ProtoMessage { float current_humidity{0.0f}; float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1116,6 +1169,7 @@ class ClimateCommandRequest : public ProtoMessage { bool has_target_humidity{false}; float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1141,6 +1195,7 @@ class ListEntitiesNumberResponse : public ProtoMessage { enums::NumberMode mode{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1156,6 +1211,7 @@ class NumberStateResponse : public ProtoMessage { float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1169,6 +1225,7 @@ class NumberCommandRequest : public ProtoMessage { uint32_t key{0}; float state{0.0f}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1187,6 +1244,7 @@ class ListEntitiesSelectResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1202,6 +1260,7 @@ class SelectStateResponse : public ProtoMessage { std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1216,6 +1275,7 @@ class SelectCommandRequest : public ProtoMessage { uint32_t key{0}; std::string state{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1238,6 +1298,7 @@ class ListEntitiesLockResponse : public ProtoMessage { bool requires_code{false}; std::string code_format{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1252,6 +1313,7 @@ class LockStateResponse : public ProtoMessage { uint32_t key{0}; enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1267,6 +1329,7 @@ class LockCommandRequest : public ProtoMessage { bool has_code{false}; std::string code{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1287,6 +1350,7 @@ class ListEntitiesButtonResponse : public ProtoMessage { enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1300,6 +1364,7 @@ class ButtonCommandRequest : public ProtoMessage { public: uint32_t key{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1315,6 +1380,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage { enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1335,6 +1401,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1351,6 +1418,7 @@ class MediaPlayerStateResponse : public ProtoMessage { float volume{0.0f}; bool muted{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1371,6 +1439,7 @@ class MediaPlayerCommandRequest : public ProtoMessage { bool has_announcement{false}; bool announcement{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1384,6 +1453,7 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { public: uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1397,6 +1467,7 @@ class BluetoothServiceData : public ProtoMessage { std::vector legacy_data{}; std::string data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1415,6 +1486,7 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { std::vector manufacturer_data{}; uint32_t address_type{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1430,6 +1502,7 @@ class BluetoothLERawAdvertisement : public ProtoMessage { uint32_t address_type{0}; std::string data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1442,6 +1515,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { public: std::vector advertisements{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1456,6 +1530,7 @@ class BluetoothDeviceRequest : public ProtoMessage { bool has_address_type{false}; uint32_t address_type{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1470,6 +1545,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { uint32_t mtu{0}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1481,6 +1557,7 @@ class BluetoothGATTGetServicesRequest : public ProtoMessage { public: uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1493,6 +1570,7 @@ class BluetoothGATTDescriptor : public ProtoMessage { std::vector uuid{}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1507,6 +1585,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage { uint32_t properties{0}; std::vector descriptors{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1521,6 +1600,7 @@ class BluetoothGATTService : public ProtoMessage { uint32_t handle{0}; std::vector characteristics{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1534,6 +1614,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { uint64_t address{0}; std::vector services{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1546,6 +1627,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { public: uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1558,6 +1640,7 @@ class BluetoothGATTReadRequest : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1571,6 +1654,7 @@ class BluetoothGATTReadResponse : public ProtoMessage { uint32_t handle{0}; std::string data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1586,6 +1670,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage { bool response{false}; std::string data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1599,6 +1684,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1612,6 +1698,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { uint32_t handle{0}; std::string data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1626,6 +1713,7 @@ class BluetoothGATTNotifyRequest : public ProtoMessage { uint32_t handle{0}; bool enable{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1639,6 +1727,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { uint32_t handle{0}; std::string data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1650,6 +1739,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1662,6 +1752,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { uint32_t limit{0}; std::vector allocated{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1675,6 +1766,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { uint32_t handle{0}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1687,6 +1779,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1699,6 +1792,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1712,6 +1806,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { bool paired{false}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1725,6 +1820,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { bool success{false}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1735,6 +1831,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1747,6 +1844,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { bool success{false}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1759,6 +1857,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1770,6 +1869,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { public: enums::BluetoothScannerMode mode{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1782,6 +1882,7 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { bool subscribe{false}; uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1795,6 +1896,7 @@ class VoiceAssistantAudioSettings : public ProtoMessage { uint32_t auto_gain{0}; float volume_multiplier{0.0f}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1811,6 +1913,7 @@ class VoiceAssistantRequest : public ProtoMessage { VoiceAssistantAudioSettings audio_settings{}; std::string wake_word_phrase{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1824,6 +1927,7 @@ class VoiceAssistantResponse : public ProtoMessage { uint32_t port{0}; bool error{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1836,6 +1940,7 @@ class VoiceAssistantEventData : public ProtoMessage { std::string name{}; std::string value{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1848,6 +1953,7 @@ class VoiceAssistantEventResponse : public ProtoMessage { enums::VoiceAssistantEvent event_type{}; std::vector data{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1861,6 +1967,7 @@ class VoiceAssistantAudio : public ProtoMessage { std::string data{}; bool end{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1878,6 +1985,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { uint32_t seconds_left{0}; bool is_active{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1893,6 +2001,7 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage { std::string preannounce_media_id{}; bool start_conversation{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1905,6 +2014,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { public: bool success{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1918,6 +2028,7 @@ class VoiceAssistantWakeWord : public ProtoMessage { std::string wake_word{}; std::vector trained_languages{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1928,6 +2039,7 @@ class VoiceAssistantWakeWord : public ProtoMessage { class VoiceAssistantConfigurationRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1940,6 +2052,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { std::vector active_wake_words{}; uint32_t max_active_wake_words{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1952,6 +2065,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { public: std::vector active_wake_words{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1972,6 +2086,7 @@ class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { bool requires_code{false}; bool requires_code_to_arm{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1986,6 +2101,7 @@ class AlarmControlPanelStateResponse : public ProtoMessage { uint32_t key{0}; enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2000,6 +2116,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { enums::AlarmControlPanelStateCommand command{}; std::string code{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2023,6 +2140,7 @@ class ListEntitiesTextResponse : public ProtoMessage { std::string pattern{}; enums::TextMode mode{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2038,6 +2156,7 @@ class TextStateResponse : public ProtoMessage { std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2052,6 +2171,7 @@ class TextCommandRequest : public ProtoMessage { uint32_t key{0}; std::string state{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2070,6 +2190,7 @@ class ListEntitiesDateResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2087,6 +2208,7 @@ class DateStateResponse : public ProtoMessage { uint32_t month{0}; uint32_t day{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2102,6 +2224,7 @@ class DateCommandRequest : public ProtoMessage { uint32_t month{0}; uint32_t day{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2120,6 +2243,7 @@ class ListEntitiesTimeResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2137,6 +2261,7 @@ class TimeStateResponse : public ProtoMessage { uint32_t minute{0}; uint32_t second{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2152,6 +2277,7 @@ class TimeCommandRequest : public ProtoMessage { uint32_t minute{0}; uint32_t second{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2172,6 +2298,7 @@ class ListEntitiesEventResponse : public ProtoMessage { std::string device_class{}; std::vector event_types{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2186,6 +2313,7 @@ class EventResponse : public ProtoMessage { uint32_t key{0}; std::string event_type{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2208,6 +2336,7 @@ class ListEntitiesValveResponse : public ProtoMessage { bool supports_position{false}; bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2223,6 +2352,7 @@ class ValveStateResponse : public ProtoMessage { float position{0.0f}; enums::ValveOperation current_operation{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2238,6 +2368,7 @@ class ValveCommandRequest : public ProtoMessage { float position{0.0f}; bool stop{false}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2256,6 +2387,7 @@ class ListEntitiesDateTimeResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2271,6 +2403,7 @@ class DateTimeStateResponse : public ProtoMessage { bool missing_state{false}; uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2284,6 +2417,7 @@ class DateTimeCommandRequest : public ProtoMessage { uint32_t key{0}; uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2302,6 +2436,7 @@ class ListEntitiesUpdateResponse : public ProtoMessage { enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2324,6 +2459,7 @@ class UpdateStateResponse : public ProtoMessage { std::string release_summary{}; std::string release_url{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2338,6 +2474,7 @@ class UpdateCommandRequest : public ProtoMessage { uint32_t key{0}; enums::UpdateCommand command{}; void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index dd86c9538a..cde4492000 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,5 +1,5 @@ // This file was automatically generated with a tool. -// See scripts/api_protobuf/api_protobuf.py +// See script/api_protobuf/api_protobuf.py #include "api_pb2_service.h" #include "esphome/core/log.h" diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1012d8a65b..4964f41420 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -1,5 +1,5 @@ // This file was automatically generated with a tool. -// See scripts/api_protobuf/api_protobuf.py +// See script/api_protobuf/api_protobuf.py #pragma once #include "api_pb2.h" diff --git a/esphome/components/api/api_pb2_size.h b/esphome/components/api/api_pb2_size.h new file mode 100644 index 0000000000..e591a7350f --- /dev/null +++ b/esphome/components/api/api_pb2_size.h @@ -0,0 +1,361 @@ +#pragma once + +#include "proto.h" +#include +#include + +namespace esphome { +namespace api { + +class ProtoSize { + public: + /** + * @brief ProtoSize class for Protocol Buffer serialization size calculation + * + * This class provides static methods to calculate the exact byte counts needed + * for encoding various Protocol Buffer field types. All methods are designed to be + * efficient for the common case where many fields have default values. + * + * Implements Protocol Buffer encoding size calculation according to: + * https://protobuf.dev/programming-guides/encoding/ + * + * Key features: + * - Early-return optimization for zero/default values + * - Direct total_size updates to avoid unnecessary additions + * - Specialized handling for different field types according to protobuf spec + * - Templated helpers for repeated fields and messages + */ + + /** + * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint + * + * @param value The uint32_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(uint32_t value) { + // Optimized varint size calculation using leading zeros + // Each 7 bits requires one byte in the varint encoding + if (value < 128) + return 1; // 7 bits, common case for small values + + // For larger values, count bytes needed based on the position of the highest bit set + if (value < 16384) { + return 2; // 14 bits + } else if (value < 2097152) { + return 3; // 21 bits + } else if (value < 268435456) { + return 4; // 28 bits + } else { + return 5; // 32 bits (maximum for uint32_t) + } + } + + /** + * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint + * + * @param value The uint64_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(uint64_t value) { + // Handle common case of values fitting in uint32_t (vast majority of use cases) + if (value <= UINT32_MAX) { + return varint(static_cast(value)); + } + + // For larger values, determine size based on highest bit position + if (value < (1ULL << 35)) { + return 5; // 35 bits + } else if (value < (1ULL << 42)) { + return 6; // 42 bits + } else if (value < (1ULL << 49)) { + return 7; // 49 bits + } else if (value < (1ULL << 56)) { + return 8; // 56 bits + } else if (value < (1ULL << 63)) { + return 9; // 63 bits + } else { + return 10; // 64 bits (maximum for uint64_t) + } + } + + /** + * @brief Calculates the size in bytes needed to encode an int32_t value as a varint + * + * Special handling is needed for negative values, which are sign-extended to 64 bits + * in Protocol Buffers, resulting in a 10-byte varint. + * + * @param value The int32_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(int32_t value) { + // Negative values are sign-extended to 64 bits in protocol buffers, + // which always results in a 10-byte varint for negative int32 + if (value < 0) { + return 10; // Negative int32 is always 10 bytes long + } + // For non-negative values, use the uint32_t implementation + return varint(static_cast(value)); + } + + /** + * @brief Calculates the size in bytes needed to encode an int64_t value as a varint + * + * @param value The int64_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(int64_t value) { + // For int64_t, we convert to uint64_t and calculate the size + // This works because the bit pattern determines the encoding size, + // and we've handled negative int32 values as a special case above + return varint(static_cast(value)); + } + + /** + * @brief Calculates the size in bytes needed to encode a field ID and wire type + * + * @param field_id The field identifier + * @param type The wire type value (from the WireType enum in the protobuf spec) + * @return The number of bytes needed to encode the field ID and wire type + */ + static inline uint32_t field(uint32_t field_id, uint32_t type) { + uint32_t tag = (field_id << 3) | (type & 0b111); + return varint(tag); + } + + /** + * @brief Common parameters for all add_*_field methods + * + * All add_*_field methods follow these common patterns: + * + * @param total_size Reference to the total message size to update + * @param field_id_size Pre-calculated size of the field ID in bytes + * @param value The value to calculate size for (type varies) + * @param force Whether to calculate size even if the value is default/zero/empty + * + * Each method follows this implementation pattern: + * 1. Skip calculation if value is default (0, false, empty) and not forced + * 2. Calculate the size based on the field's encoding rules + * 3. Add the field_id_size + calculated value size to total_size + */ + + /** + * @brief Calculates and adds the size of an int32 field to the total message size + */ + static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + if (value < 0) { + // Negative values are encoded as 10-byte varints in protobuf + total_size += field_id_size + 10; + } else { + // For non-negative values, use the standard varint size + total_size += field_id_size + varint(static_cast(value)); + } + } + + /** + * @brief Calculates and adds the size of a uint32 field to the total message size + */ + static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, + bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a boolean field to the total message size + */ + static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) { + // Skip calculation if value is false and not forced + if (!value && !force) { + return; // No need to update total_size + } + + // Boolean fields always use 1 byte when true + total_size += field_id_size + 1; + } + + /** + * @brief Calculates and adds the size of a fixed field to the total message size + * + * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double). + * + * @tparam NumBytes The number of bytes for this fixed field (4 or 8) + * @param is_nonzero Whether the value is non-zero + */ + template + static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero, + bool force = false) { + // Skip calculation if value is zero and not forced + if (!is_nonzero && !force) { + return; // No need to update total_size + } + + // Fixed fields always take exactly NumBytes + total_size += field_id_size + NumBytes; + } + + /** + * @brief Calculates and adds the size of an enum field to the total message size + * + * Enum fields are encoded as uint32 varints. + */ + static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // Enums are encoded as uint32 + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a sint32 field to the total message size + * + * Sint32 fields use ZigZag encoding, which is more efficient for negative values. + */ + static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) + uint32_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 31)); + total_size += field_id_size + varint(zigzag); + } + + /** + * @brief Calculates and adds the size of an int64 field to the total message size + */ + static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a uint64 field to the total message size + */ + static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value, + bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a sint64 field to the total message size + * + * Sint64 fields use ZigZag encoding, which is more efficient for negative values. + */ + static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { + // Skip calculation if value is zero and not forced + if (value == 0 && !force) { + return; // No need to update total_size + } + + // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) + uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); + total_size += field_id_size + varint(zigzag); + } + + /** + * @brief Calculates and adds the size of a string/bytes field to the total message size + */ + static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str, + bool force = false) { + // Skip calculation if string is empty and not forced + if (str.empty() && !force) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + const uint32_t str_size = static_cast(str.size()); + total_size += field_id_size + varint(str_size) + str_size; + } + + /** + * @brief Calculates and adds the size of a nested message field to the total message size + * + * This helper function directly updates the total_size reference if the nested size + * is greater than zero or force is true. + * + * @param nested_size The pre-calculated size of the nested message + */ + static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size, + bool force = false) { + // Skip calculation if nested message is empty and not forced + if (nested_size == 0 && !force) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + // Field ID + length varint + nested message content + total_size += field_id_size + varint(nested_size) + nested_size; + } + + /** + * @brief Calculates and adds the size of a nested message field to the total message size + * + * This templated version directly takes a message object, calculates its size internally, + * and updates the total_size reference. This eliminates the need for a temporary variable + * at the call site. + * + * @tparam MessageType The type of the nested message (inferred from parameter) + * @param message The nested message object + */ + template + static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message, + bool force = false) { + uint32_t nested_size = 0; + message.calculate_size(nested_size); + + // Use the base implementation with the calculated nested_size + add_message_field(total_size, field_id_size, nested_size, force); + } + + /** + * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size + * + * This helper processes a vector of message objects, calculating the size for each message + * and adding it to the total size. + * + * @tparam MessageType The type of the nested messages in the vector + * @param messages Vector of message objects + */ + template + static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size, + const std::vector &messages) { + // Skip if the vector is empty + if (messages.empty()) { + return; + } + + // For repeated fields, always use force=true + for (const auto &message : messages) { + add_message_object(total_size, field_id_size, message, true); + } + } +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index ccc6c0d52c..81e20d47be 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -276,6 +276,7 @@ class ProtoMessage { virtual ~ProtoMessage() = default; virtual void encode(ProtoWriteBuffer buffer) const = 0; void decode(const uint8_t *buffer, size_t length); + virtual void calculate_size(uint32_t &total_size) const = 0; #ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; @@ -302,9 +303,19 @@ class ProtoService { virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0; virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + // Optimized method that pre-allocates buffer based on message size template bool send_message_(const C &msg, uint32_t message_type) { + uint32_t msg_size = 0; + msg.calculate_size(msg_size); + + // Create a pre-sized buffer auto buffer = this->create_buffer(); + buffer.get_buffer()->reserve(msg_size); + + // Encode message into the buffer msg.encode(buffer); + + // Send the buffer return this->send_buffer(buffer, message_type); } }; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 291a03523e..63c1efa1ee 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from enum import IntEnum import os from pathlib import Path import re @@ -10,11 +11,29 @@ import sys from textwrap import dedent from typing import Any -# Generate with -# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto import aioesphomeapi.api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor + +class WireType(IntEnum): + """Protocol Buffer wire types as defined in the protobuf spec. + + As specified in the Protocol Buffers encoding guide: + https://protobuf.dev/programming-guides/encoding/#structure + """ + + VARINT = 0 # int32, int64, uint32, uint64, sint32, sint64, bool, enum + FIXED64 = 1 # fixed64, sfixed64, double + LENGTH_DELIMITED = 2 # string, bytes, embedded messages, packed repeated fields + START_GROUP = 3 # groups (deprecated) + END_GROUP = 4 # groups (deprecated) + FIXED32 = 5 # fixed32, sfixed32, float + + +# Generate with +# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto + + """Python 3 script to automatically generate C++ classes for ESPHome's native API. It's pretty crappy spaghetti code, but it works. @@ -35,7 +54,7 @@ will be generated, they still need to be formatted FILE_HEADER = """// This file was automatically generated with a tool. -// See scripts/api_protobuf/api_protobuf.py +// See script/api_protobuf/api_protobuf.py """ @@ -63,6 +82,11 @@ def camel_to_snake(name: str) -> str: return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() +def force_str(force: bool) -> str: + """Convert a boolean force value to string format for C++ code.""" + return str(force).lower() + + class TypeInfo(ABC): """Base class for all type information.""" @@ -99,6 +123,11 @@ class TypeInfo(ABC): """Check if the field is repeated.""" return self._field.label == 3 + @property + def wire_type(self) -> WireType: + """Get the wire type for the field.""" + raise NotImplementedError + @property def cpp_type(self) -> str: raise NotImplementedError @@ -200,6 +229,35 @@ class TypeInfo(ABC): def dump(self, name: str) -> str: """Dump the value to the output.""" + def calculate_field_id_size(self) -> int: + """Calculates the size of a field ID in bytes. + + Returns: + The number of bytes needed to encode the field ID + """ + # Calculate the tag by combining field_id and wire_type + tag = (self.number << 3) | (self.wire_type & 0b111) + + # Calculate the varint size + if tag < 128: + return 1 # 7 bits + if tag < 16384: + return 2 # 14 bits + if tag < 2097152: + return 3 # 21 bits + if tag < 268435456: + return 4 # 28 bits + return 5 # 32 bits (maximum for uint32_t) + + @abstractmethod + def get_size_calculation(self, name: str, force: bool = False) -> str: + """Calculate the size needed for encoding this field. + + Args: + name: The name of the field + force: Whether to force encoding the field even if it has a default value + """ + TYPE_INFO: dict[int, TypeInfo] = {} @@ -221,12 +279,18 @@ class DoubleType(TypeInfo): default_value = "0.0" decode_64bit = "value.as_double()" encode_func = "encode_double" + wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec def dump(self, name: str) -> str: o = f'sprintf(buffer, "%g", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0.0, {force_str(force)});" + return o + @register_type(2) class FloatType(TypeInfo): @@ -234,12 +298,18 @@ class FloatType(TypeInfo): default_value = "0.0f" decode_32bit = "value.as_float()" encode_func = "encode_float" + wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%g", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0.0f, {force_str(force)});" + return o + @register_type(3) class Int64Type(TypeInfo): @@ -247,12 +317,18 @@ class Int64Type(TypeInfo): default_value = "0" decode_varint = "value.as_int64()" encode_func = "encode_int64" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%lld", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_int64_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(4) class UInt64Type(TypeInfo): @@ -260,12 +336,18 @@ class UInt64Type(TypeInfo): default_value = "0" decode_varint = "value.as_uint64()" encode_func = "encode_uint64" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%llu", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_uint64_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(5) class Int32Type(TypeInfo): @@ -273,12 +355,18 @@ class Int32Type(TypeInfo): default_value = "0" decode_varint = "value.as_int32()" encode_func = "encode_int32" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRId32, {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_int32_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(6) class Fixed64Type(TypeInfo): @@ -286,12 +374,18 @@ class Fixed64Type(TypeInfo): default_value = "0" decode_64bit = "value.as_fixed64()" encode_func = "encode_fixed64" + wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%llu", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" + return o + @register_type(7) class Fixed32Type(TypeInfo): @@ -299,12 +393,18 @@ class Fixed32Type(TypeInfo): default_value = "0" decode_32bit = "value.as_fixed32()" encode_func = "encode_fixed32" + wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRIu32, {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" + return o + @register_type(8) class BoolType(TypeInfo): @@ -312,11 +412,17 @@ class BoolType(TypeInfo): default_value = "false" decode_varint = "value.as_bool()" encode_func = "encode_bool" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f"out.append(YESNO({name}));" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_bool_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(9) class StringType(TypeInfo): @@ -326,11 +432,17 @@ class StringType(TypeInfo): const_reference_type = "const std::string &" decode_length = "value.as_string()" encode_func = "encode_string" + wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 def dump(self, name): o = f'out.append("\'").append({name}).append("\'");' return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_string_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(11) class MessageType(TypeInfo): @@ -339,6 +451,7 @@ class MessageType(TypeInfo): return self._field.type_name[1:] default_value = "" + wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 @property def reference_type(self) -> str: @@ -360,6 +473,11 @@ class MessageType(TypeInfo): o = f"{name}.dump_to(out);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_message_object(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(12) class BytesType(TypeInfo): @@ -369,11 +487,17 @@ class BytesType(TypeInfo): const_reference_type = "const std::string &" decode_length = "value.as_string()" encode_func = "encode_string" + wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 def dump(self, name: str) -> str: o = f'out.append("\'").append({name}).append("\'");' return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_string_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(13) class UInt32Type(TypeInfo): @@ -381,12 +505,18 @@ class UInt32Type(TypeInfo): default_value = "0" decode_varint = "value.as_uint32()" encode_func = "encode_uint32" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRIu32, {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_uint32_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(14) class EnumType(TypeInfo): @@ -399,6 +529,7 @@ class EnumType(TypeInfo): return f"value.as_enum<{self.cpp_type}>()" default_value = "" + wire_type = WireType.VARINT # Uses wire type 0 @property def encode_func(self) -> str: @@ -408,6 +539,11 @@ class EnumType(TypeInfo): o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_enum_field(total_size, {field_id_size}, static_cast({name}), {force_str(force)});" + return o + @register_type(15) class SFixed32Type(TypeInfo): @@ -415,12 +551,18 @@ class SFixed32Type(TypeInfo): default_value = "0" decode_32bit = "value.as_sfixed32()" encode_func = "encode_sfixed32" + wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRId32, {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" + return o + @register_type(16) class SFixed64Type(TypeInfo): @@ -428,12 +570,18 @@ class SFixed64Type(TypeInfo): default_value = "0" decode_64bit = "value.as_sfixed64()" encode_func = "encode_sfixed64" + wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%lld", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" + return o + @register_type(17) class SInt32Type(TypeInfo): @@ -441,12 +589,18 @@ class SInt32Type(TypeInfo): default_value = "0" decode_varint = "value.as_sint32()" encode_func = "encode_sint32" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRId32, {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_sint32_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + @register_type(18) class SInt64Type(TypeInfo): @@ -454,12 +608,18 @@ class SInt64Type(TypeInfo): default_value = "0" decode_varint = "value.as_sint64()" encode_func = "encode_sint64" + wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: o = f'sprintf(buffer, "%lld", {name});\n' o += "out.append(buffer);" return o + def get_size_calculation(self, name: str, force: bool = False) -> str: + field_id_size = self.calculate_field_id_size() + o = f"ProtoSize::add_sint64_field(total_size, {field_id_size}, {name}, {force_str(force)});" + return o + class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: @@ -478,6 +638,14 @@ class RepeatedTypeInfo(TypeInfo): def const_reference_type(self) -> str: return f"const {self.cpp_type} &" + @property + def wire_type(self) -> WireType: + """Get the wire type for this repeated field. + + For repeated fields, we use the same wire type as the underlying field. + """ + return self._ti.wire_type + @property def decode_varint_content(self) -> str: content = self._ti.decode_varint @@ -554,6 +722,22 @@ class RepeatedTypeInfo(TypeInfo): def dump(self, _: str): pass + def get_size_calculation(self, name: str, force: bool = False) -> str: + # For repeated fields, we always need to pass force=True to the underlying type's calculation + # This is because the encode method always sets force=true for repeated fields + if isinstance(self._ti, MessageType): + # For repeated messages, use the dedicated helper that handles iteration internally + field_id_size = self._ti.calculate_field_id_size() + o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});" + return o + # For other repeated types, use the underlying type's size calculation with force=True + o = f"if (!{name}.empty()) {{\n" + o += f" for (const auto {'' if self._ti_is_bool else '&'}it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += " }\n" + o += "}" + return o + def build_enum_type(desc) -> tuple[str, str]: """Builds the enum type.""" @@ -587,6 +771,7 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: decode_64bit: list[str] = [] encode: list[str] = [] dump: list[str] = [] + size_calc: list[str] = [] for field in desc.field: if field.label == 3: @@ -596,6 +781,7 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: protected_content.extend(ti.protected_content) public_content.extend(ti.public_content) encode.append(ti.encode_content) + size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}")) if ti.decode_varint_content: decode_varint.append(ti.decode_varint_content) @@ -662,6 +848,25 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: prot = "void encode(ProtoWriteBuffer buffer) const override;" public_content.append(prot) + # Add calculate_size method + o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{" + + # Add a check for empty/default objects to short-circuit the calculation + # Only add this optimization if we have fields to check + if size_calc: + # For a single field, just inline it for simplicity + if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120: + o += f" {size_calc[0]} " + else: + # For multiple fields, add a short-circuit check + o += "\n" + # Performance optimization: add all the size calculations + o += indent("\n".join(size_calc)) + "\n" + o += "}\n" + cpp += o + prot = "void calculate_size(uint32_t &total_size) const override;" + public_content.append(prot) + o = f"void {desc.name}::dump_to(std::string &out) const {{" if dump: if len(dump) == 1 and len(dump[0]) + len(o) + 3 < 120: @@ -796,6 +1001,7 @@ def main() -> None: #pragma once #include "proto.h" + #include "api_pb2_size.h" namespace esphome { namespace api { @@ -805,6 +1011,7 @@ def main() -> None: cpp = FILE_HEADER cpp += """\ #include "api_pb2.h" + #include "api_pb2_size.h" #include "esphome/core/log.h" #include From 26669bd1b63d7231623a0445d26335614f5ee648 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 May 2025 23:00:34 -0500 Subject: [PATCH 010/193] Preallocate Buffer Space for ESP32-CAM (#8712) --- esphome/components/api/api_connection.cpp | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4670aeca63..06d3b61692 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -192,15 +192,35 @@ void APIConnection::loop() { #ifdef USE_ESP32_CAMERA if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { - uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); + // Message will use 8 more bytes than the minimum size, and typical + // MTU is 1500. Sometimes users will see as low as 1460 MTU. + // If its IPv6 the header is 40 bytes, and if its IPv4 + // the header is 20 bytes. So we have 1460 - 40 = 1420 bytes + // available for the payload. But we also need to add the size of + // the protobuf overhead, which is 8 bytes. + // + // To be safe we pick 1390 bytes as the maximum size + // to send in one go. This is the maximum size of a single packet + // that can be sent over the network. + // This is to avoid fragmentation of the packet. + uint32_t to_send = std::min((size_t) 1390, this->image_reader_.available()); + bool done = this->image_reader_.available() == to_send; + uint32_t msg_size = 0; + ProtoSize::add_fixed_field<4>(msg_size, 1, true); + // partial message size calculated manually since its a special case + // 1 for the data field, varint for the data size, and the data itself + msg_size += 1 + ProtoSize::varint(to_send) + to_send; + ProtoSize::add_bool_field(msg_size, 1, done); + auto buffer = this->create_buffer(); + buffer.get_buffer()->reserve(msg_size); // fixed32 key = 1; buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); // bytes data = 2; buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); // bool done = 3; - bool done = this->image_reader_.available() == to_send; buffer.encode_bool(3, done); + bool success = this->send_buffer(buffer, 44); if (success) { From ad2b74d9b49ec18c90bfc9f6404161faf29c1f78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 May 2025 23:01:10 -0500 Subject: [PATCH 011/193] Correct Protobuf Wire Type for `encode_fixed64` (#8713) --- esphome/components/api/proto.h | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 81e20d47be..bfa1a5c280 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -149,6 +149,18 @@ class ProtoWriteBuffer { void write(uint8_t value) { this->buffer_->push_back(value); } void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } + /** + * Encode a field key (tag/wire type combination). + * + * @param field_id Field number (tag) in the protobuf message + * @param type Wire type value: + * - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) + * - 1: 64-bit (fixed64, sfixed64, double) + * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields) + * - 5: 32-bit (fixed32, sfixed32, float) + * + * Following https://protobuf.dev/programming-guides/encoding/#structure + */ void encode_field_raw(uint32_t field_id, uint32_t type) { uint32_t val = (field_id << 3) | (type & 0b111); this->encode_varint_raw(val); @@ -157,7 +169,7 @@ class ProtoWriteBuffer { if (len == 0 && !force) return; - this->encode_field_raw(field_id, 2); + this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); auto *data = reinterpret_cast(string); this->buffer_->insert(this->buffer_->end(), data, data + len); @@ -171,26 +183,26 @@ class ProtoWriteBuffer { void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; - this->encode_field_raw(field_id, 0); + this->encode_field_raw(field_id, 0); // type 0: Varint - uint32 this->encode_varint_raw(value); } void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { if (value == 0 && !force) return; - this->encode_field_raw(field_id, 0); + this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 this->encode_varint_raw(ProtoVarInt(value)); } void encode_bool(uint32_t field_id, bool value, bool force = false) { if (!value && !force) return; - this->encode_field_raw(field_id, 0); + this->encode_field_raw(field_id, 0); // type 0: Varint - bool this->write(0x01); } void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; - this->encode_field_raw(field_id, 5); + this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 this->write((value >> 0) & 0xFF); this->write((value >> 8) & 0xFF); this->write((value >> 16) & 0xFF); @@ -200,7 +212,7 @@ class ProtoWriteBuffer { if (value == 0 && !force) return; - this->encode_field_raw(field_id, 5); + this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64 this->write((value >> 0) & 0xFF); this->write((value >> 8) & 0xFF); this->write((value >> 16) & 0xFF); @@ -254,7 +266,7 @@ class ProtoWriteBuffer { this->encode_uint64(field_id, uvalue, force); } template void encode_message(uint32_t field_id, const C &value, bool force = false) { - this->encode_field_raw(field_id, 2); + this->encode_field_raw(field_id, 2); // type 2: Length-delimited message size_t begin = this->buffer_->size(); value.encode(*this); From 882273cb560a4188a4965ecbfd85ee87cbd6634c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 May 2025 23:19:53 -0500 Subject: [PATCH 012/193] Avoid Reallocation When Sending Logging Messages (#8714) --- esphome/components/api/api_connection.cpp | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 06d3b61692..c444307bd6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1794,12 +1794,26 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char if (this->log_subscription_ < level) return false; - // Send raw so that we don't copy too much + // Pre-calculate message size to avoid reallocations + const size_t line_length = strlen(line); + uint32_t msg_size = 0; + + // Add size for level field (field ID 1, varint type) + // 1 byte for field tag + size of the level varint + msg_size += 1 + api::ProtoSize::varint(static_cast(level)); + + // Add size for string field (field ID 3, string type) + // 1 byte for field tag + size of length varint + string length + msg_size += 1 + api::ProtoSize::varint(static_cast(line_length)) + line_length; + + // Create a pre-sized buffer auto buffer = this->create_buffer(); - // LogLevel level = 1; - buffer.encode_uint32(1, static_cast(level)); - // string message = 3; - buffer.encode_string(3, line, strlen(line)); + buffer.get_buffer()->reserve(msg_size); + + // Encode the message (SubscribeLogsResponse) + buffer.encode_uint32(1, static_cast(level)); // LogLevel level = 1 + buffer.encode_string(3, line, line_length); // string message = 3 + // SubscribeLogsResponse - 29 return this->send_buffer(buffer, 29); } From ef2621aa5470db7af36e3b39b4a1f1cc70da2c7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 00:43:39 -0500 Subject: [PATCH 013/193] Reserve space in the frame helper when we know in advance how much we need (#8716) --- esphome/components/api/api_frame_helper.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 3d6bc95163..e9751ac8dc 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -5,6 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/application.h" #include "proto.h" +#include "api_pb2_size.h" #include namespace esphome { @@ -574,6 +575,8 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { if (!tx_buf_.empty()) { // tx buf not empty, can't write now because then stream would be inconsistent + // Reserve space upfront to avoid multiple reallocations + tx_buf_.reserve(tx_buf_.size() + total_write_len); for (int i = 0; i < iovcnt; i++) { tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); @@ -584,6 +587,8 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { ssize_t sent = socket_->writev(iov, iovcnt); if (is_would_block(sent)) { // operation would block, add buffer to tx_buf + // Reserve space upfront to avoid multiple reallocations + tx_buf_.reserve(tx_buf_.size() + total_write_len); for (int i = 0; i < iovcnt; i++) { tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); @@ -596,6 +601,10 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { return APIError::SOCKET_WRITE_FAILED; } else if ((size_t) sent != total_write_len) { // partially sent, add end to tx_buf + size_t remaining = total_write_len - sent; + // Reserve space upfront to avoid multiple reallocations + tx_buf_.reserve(tx_buf_.size() + remaining); + size_t to_consume = sent; for (int i = 0; i < iovcnt; i++) { if (to_consume >= iov[i].iov_len) { @@ -933,6 +942,8 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay } std::vector header; + header.reserve(1 + api::ProtoSize::varint(static_cast(payload_len)) + + api::ProtoSize::varint(static_cast(type))); header.push_back(0x00); ProtoVarInt(payload_len).encode(header); ProtoVarInt(type).encode(header); @@ -994,6 +1005,8 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt if (!tx_buf_.empty()) { // tx buf not empty, can't write now because then stream would be inconsistent + // Reserve space upfront to avoid multiple reallocations + tx_buf_.reserve(tx_buf_.size() + total_write_len); for (int i = 0; i < iovcnt; i++) { tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); @@ -1004,6 +1017,8 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt ssize_t sent = socket_->writev(iov, iovcnt); if (is_would_block(sent)) { // operation would block, add buffer to tx_buf + // Reserve space upfront to avoid multiple reallocations + tx_buf_.reserve(tx_buf_.size() + total_write_len); for (int i = 0; i < iovcnt; i++) { tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); @@ -1016,6 +1031,10 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt return APIError::SOCKET_WRITE_FAILED; } else if ((size_t) sent != total_write_len) { // partially sent, add end to tx_buf + size_t remaining = total_write_len - sent; + // Reserve space upfront to avoid multiple reallocations + tx_buf_.reserve(tx_buf_.size() + remaining); + size_t to_consume = sent; for (int i = 0; i < iovcnt; i++) { if (to_consume >= iov[i].iov_len) { From 9e64e71cdf8bf769245539dc7899726a910eba09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 00:50:20 -0500 Subject: [PATCH 014/193] Require reserve_size in create_buffer to reduce realloc overhead (#8715) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 6 ++---- esphome/components/api/api_connection.h | 3 ++- esphome/components/api/proto.h | 12 +++++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c444307bd6..ee0451f499 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -212,8 +212,7 @@ void APIConnection::loop() { msg_size += 1 + ProtoSize::varint(to_send) + to_send; ProtoSize::add_bool_field(msg_size, 1, done); - auto buffer = this->create_buffer(); - buffer.get_buffer()->reserve(msg_size); + auto buffer = this->create_buffer(msg_size); // fixed32 key = 1; buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); // bytes data = 2; @@ -1807,8 +1806,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char msg_size += 1 + api::ProtoSize::varint(static_cast(line_length)) + line_length; // Create a pre-sized buffer - auto buffer = this->create_buffer(); - buffer.get_buffer()->reserve(msg_size); + auto buffer = this->create_buffer(msg_size); // Encode the message (SubscribeLogsResponse) buffer.encode_uint32(1, static_cast(level)); // LogLevel level = 1 diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3fefe71cbb..1e47418d90 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -312,9 +312,10 @@ class APIConnection : public APIServerConnection { void on_fatal_error() override; void on_unauthenticated_access() override; void on_no_setup_connection() override; - ProtoWriteBuffer create_buffer() override { + ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { // FIXME: ensure no recursive writes can happen this->proto_write_buffer_.clear(); + this->proto_write_buffer_.reserve(reserve_size); return {&this->proto_write_buffer_}; } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index bfa1a5c280..b8ee6b7920 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -311,7 +311,14 @@ class ProtoService { virtual void on_fatal_error() = 0; virtual void on_unauthenticated_access() = 0; virtual void on_no_setup_connection() = 0; - virtual ProtoWriteBuffer create_buffer() = 0; + /** + * Create a buffer with a reserved size. + * @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint + * to optimize memory usage and avoid reallocations during encoding. + * Implementations should aim to allocate at least this size. + * @return A ProtoWriteBuffer object with the reserved size. + */ + virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0; virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; @@ -321,8 +328,7 @@ class ProtoService { msg.calculate_size(msg_size); // Create a pre-sized buffer - auto buffer = this->create_buffer(); - buffer.get_buffer()->reserve(msg_size); + auto buffer = this->create_buffer(msg_size); // Encode message into the buffer msg.encode(buffer); From 8e29437900927607b607d9ce235444e8cdcd3f84 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Thu, 8 May 2025 01:26:10 -0700 Subject: [PATCH 015/193] [key_collector] enable/disable (#8718) Co-authored-by: Samuel Sieb --- esphome/components/key_collector/__init__.py | 35 +++++++++++++++++++ .../key_collector/key_collector.cpp | 8 +++++ .../components/key_collector/key_collector.h | 10 ++++++ tests/components/key_collector/common.yaml | 8 +++++ 4 files changed, 61 insertions(+) diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index 5750812f5c..17af40da1a 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import key_provider import esphome.config_validation as cv from esphome.const import ( + CONF_ENABLE_ON_BOOT, CONF_ID, CONF_MAX_LENGTH, CONF_MIN_LENGTH, @@ -28,6 +29,8 @@ CONF_ON_RESULT = "on_result" key_collector_ns = cg.esphome_ns.namespace("key_collector") KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component) +EnableAction = key_collector_ns.class_("EnableAction", automation.Action) +DisableAction = key_collector_ns.class_("DisableAction", automation.Action) CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( @@ -46,6 +49,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True), cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, } ), cv.has_at_least_one_key(CONF_END_KEYS, CONF_MAX_LENGTH), @@ -94,3 +98,34 @@ async def to_code(config): ) if CONF_TIMEOUT in config: cg.add(var.set_timeout(config[CONF_TIMEOUT])) + cg.add(var.set_enabled(config[CONF_ENABLE_ON_BOOT])) + + +@automation.register_action( + "key_collector.enable", + EnableAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(KeyCollector), + } + ), +) +async def enable_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "key_collector.disable", + DisableAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(KeyCollector), + } + ), +) +async def disable_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp index bf2333d97d..ffb4b47fa2 100644 --- a/esphome/components/key_collector/key_collector.cpp +++ b/esphome/components/key_collector/key_collector.cpp @@ -45,6 +45,12 @@ void KeyCollector::set_provider(key_provider::KeyProvider *provider) { provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); }); } +void KeyCollector::set_enabled(bool enabled) { + this->enabled_ = enabled; + if (!enabled) + this->clear(false); +} + void KeyCollector::clear(bool progress_update) { this->result_.clear(); this->start_key_ = 0; @@ -55,6 +61,8 @@ void KeyCollector::clear(bool progress_update) { void KeyCollector::send_key(uint8_t key) { this->key_pressed_(key); } void KeyCollector::key_pressed_(uint8_t key) { + if (!this->enabled_) + return; this->last_key_time_ = millis(); if (!this->start_keys_.empty() && !this->start_key_) { if (this->start_keys_.find(key) != std::string::npos) { diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 7ef53929ef..6e585ddd8e 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -25,6 +25,7 @@ class KeyCollector : public Component { Trigger *get_result_trigger() const { return this->result_trigger_; }; Trigger *get_timeout_trigger() const { return this->timeout_trigger_; }; void set_timeout(int timeout) { this->timeout_ = timeout; }; + void set_enabled(bool enabled); void clear(bool progress_update = true); void send_key(uint8_t key); @@ -47,6 +48,15 @@ class KeyCollector : public Component { Trigger *timeout_trigger_; uint32_t last_key_time_; uint32_t timeout_{0}; + bool enabled_; +}; + +template class EnableAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_enabled(true); } +}; + +template class DisableAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_enabled(false); } }; } // namespace key_collector diff --git a/tests/components/key_collector/common.yaml b/tests/components/key_collector/common.yaml index d58922ca91..12e541c865 100644 --- a/tests/components/key_collector/common.yaml +++ b/tests/components/key_collector/common.yaml @@ -26,3 +26,11 @@ key_collector: - logger.log: format: "input timeout: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + enable_on_boot: false + +button: + - platform: template + id: button0 + on_press: + - key_collector.enable: + - key_collector.disable: From 797a4c61f22cb6fd4d7abd9fc3b4817415179c8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 15:01:52 -0500 Subject: [PATCH 016/193] Bump ruff from 0.11.7 to 0.11.8 (#8721) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6478ab2017..a920ba0edc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.2.0 # also change in .pre-commit-config.yaml when updating -ruff==0.11.7 # also change in .pre-commit-config.yaml when updating +ruff==0.11.8 # also change in .pre-commit-config.yaml when updating pyupgrade==3.19.1 # also change in .pre-commit-config.yaml when updating pre-commit From b01d85a9749971783041557e99ad8035e3b61447 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 15:02:16 -0500 Subject: [PATCH 017/193] Bump puremagic from 1.28 to 1.29 (#8722) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1f71ce9b16..d95edd1e91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ click==8.1.7 esphome-dashboard==20250415.0 aioesphomeapi==30.1.0 zeroconf==0.146.5 -puremagic==1.28 +puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import esphome-glyphsets==0.2.0 pillow==10.4.0 From 782d748210eec051ede142f9ef8095c52d92efa2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 19:05:59 -0500 Subject: [PATCH 018/193] Increase zeroconf timeout to 10 seconds (#8670) --- esphome/zeroconf.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index b235f06786..c6a143a42f 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -18,6 +18,8 @@ from esphome.storage_json import StorageJSON, ext_storage_path _LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEOUT = 10.0 +DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT * 1000 _BACKGROUND_TASKS: set[asyncio.Task] = set() @@ -107,7 +109,7 @@ class DashboardImportDiscovery: self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str ) -> None: """Process a service info.""" - if await info.async_request(zeroconf, timeout=3000): + if await info.async_request(zeroconf, timeout=DEFAULT_TIMEOUT_MS): self._process_service_info(name, info) def _process_service_info(self, name: str, info: ServiceInfo) -> None: @@ -164,7 +166,9 @@ class DashboardImportDiscovery: class EsphomeZeroconf(Zeroconf): - def resolve_host(self, host: str, timeout: float = 3.0) -> list[str] | None: + def resolve_host( + self, host: str, timeout: float = DEFAULT_TIMEOUT + ) -> list[str] | None: """Resolve a host name to an IP address.""" info = AddressResolver(f"{host.partition('.')[0]}.local.") if ( @@ -177,7 +181,7 @@ class EsphomeZeroconf(Zeroconf): class AsyncEsphomeZeroconf(AsyncZeroconf): async def async_resolve_host( - self, host: str, timeout: float = 3.0 + self, host: str, timeout: float = DEFAULT_TIMEOUT ) -> list[str] | None: """Resolve a host name to an IP address.""" info = AddressResolver(f"{host.partition('.')[0]}.local.") From 8465017db9eedbec1bd2b0ecfff917141613f4cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 19:10:44 -0500 Subject: [PATCH 019/193] Consolidate write_raw_ implementation to reduce code duplication (#8717) --- esphome/components/api/api_frame_helper.cpp | 241 ++++++++------------ esphome/components/api/api_frame_helper.h | 14 +- 2 files changed, 107 insertions(+), 148 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index e9751ac8dc..31b0732275 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -73,6 +73,91 @@ const char *api_error_to_str(APIError err) { return "UNKNOWN"; } +// Common implementation for writing raw data to socket +template +APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, + std::vector &tx_buf, const std::string &info, StateEnum &state, + StateEnum failed_state) { + // This method writes data to socket or buffers it + // Returns APIError::OK if successful (or would block, but data has been buffered) + // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state + + if (iovcnt == 0) + return APIError::OK; // Nothing to do, success + + size_t total_write_len = 0; + for (int i = 0; i < iovcnt; i++) { +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Sending raw: %s", + format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); +#endif + total_write_len += iov[i].iov_len; + } + + if (!tx_buf.empty()) { + // try to empty tx_buf first + while (!tx_buf.empty()) { + ssize_t sent = socket->write(tx_buf.data(), tx_buf.size()); + if (is_would_block(sent)) { + break; + } else if (sent == -1) { + ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno); + state = failed_state; + return APIError::SOCKET_WRITE_FAILED; // Socket write failed + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent); + } + } + + if (!tx_buf.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + // Reserve space upfront to avoid multiple reallocations + tx_buf.reserve(tx_buf.size() + total_write_len); + for (int i = 0; i < iovcnt; i++) { + tx_buf.insert(tx_buf.end(), reinterpret_cast(iov[i].iov_base), + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + } + return APIError::OK; // Success, data buffered + } + + ssize_t sent = socket->writev(iov, iovcnt); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + // Reserve space upfront to avoid multiple reallocations + tx_buf.reserve(tx_buf.size() + total_write_len); + for (int i = 0; i < iovcnt; i++) { + tx_buf.insert(tx_buf.end(), reinterpret_cast(iov[i].iov_base), + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + } + return APIError::OK; // Success, data buffered + } else if (sent == -1) { + // an error occurred + ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno); + state = failed_state; + return APIError::SOCKET_WRITE_FAILED; // Socket write failed + } else if ((size_t) sent != total_write_len) { + // partially sent, add end to tx_buf + size_t remaining = total_write_len - sent; + // Reserve space upfront to avoid multiple reallocations + tx_buf.reserve(tx_buf.size() + remaining); + + size_t to_consume = sent; + for (int i = 0; i < iovcnt; i++) { + if (to_consume >= iov[i].iov_len) { + to_consume -= iov[i].iov_len; + } else { + tx_buf.insert(tx_buf.end(), reinterpret_cast(iov[i].iov_base) + to_consume, + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + to_consume = 0; + } + } + return APIError::OK; // Success, data buffered + } + return APIError::OK; // Success, all data sent +} + #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) // uncomment to log raw packets //#define HELPER_LOG_PACKETS @@ -547,79 +632,6 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() { return APIError::OK; } -/** Write the data to the socket, or buffer it a write would block - * - * @param data The data to write - * @param len The length of data - */ -APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { - if (iovcnt == 0) - return APIError::OK; - APIError aerr; - - size_t total_write_len = 0; - for (int i = 0; i < iovcnt; i++) { -#ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", - format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); -#endif - total_write_len += iov[i].iov_len; - } - - if (!tx_buf_.empty()) { - // try to empty tx_buf_ first - aerr = try_send_tx_buf_(); - if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) - return aerr; - } - - if (!tx_buf_.empty()) { - // tx buf not empty, can't write now because then stream would be inconsistent - // Reserve space upfront to avoid multiple reallocations - tx_buf_.reserve(tx_buf_.size() + total_write_len); - for (int i = 0; i < iovcnt; i++) { - tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), - reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); - } - return APIError::OK; - } - - ssize_t sent = socket_->writev(iov, iovcnt); - if (is_would_block(sent)) { - // operation would block, add buffer to tx_buf - // Reserve space upfront to avoid multiple reallocations - tx_buf_.reserve(tx_buf_.size() + total_write_len); - for (int i = 0; i < iovcnt; i++) { - tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), - reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); - } - return APIError::OK; - } else if (sent == -1) { - // an error occurred - state_ = State::FAILED; - HELPER_LOG("Socket write failed with errno %d", errno); - return APIError::SOCKET_WRITE_FAILED; - } else if ((size_t) sent != total_write_len) { - // partially sent, add end to tx_buf - size_t remaining = total_write_len - sent; - // Reserve space upfront to avoid multiple reallocations - tx_buf_.reserve(tx_buf_.size() + remaining); - - size_t to_consume = sent; - for (int i = 0; i < iovcnt; i++) { - if (to_consume >= iov[i].iov_len) { - to_consume -= iov[i].iov_len; - } else { - tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base) + to_consume, - reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); - to_consume = 0; - } - } - return APIError::OK; - } - // fully sent - return APIError::OK; -} APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { uint8_t header[3]; header[0] = 0x01; // indicator @@ -753,6 +765,11 @@ void noise_rand_bytes(void *output, size_t len) { } } } + +// Explicit template instantiation for Noise +template APIError APIFrameHelper::write_raw_( + const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf_, const std::string &info, + APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state); #endif // USE_API_NOISE #ifdef USE_API_PLAINTEXT @@ -977,79 +994,6 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() { return APIError::OK; } -/** Write the data to the socket, or buffer it a write would block - * - * @param data The data to write - * @param len The length of data - */ -APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { - if (iovcnt == 0) - return APIError::OK; - APIError aerr; - - size_t total_write_len = 0; - for (int i = 0; i < iovcnt; i++) { -#ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", - format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); -#endif - total_write_len += iov[i].iov_len; - } - - if (!tx_buf_.empty()) { - // try to empty tx_buf_ first - aerr = try_send_tx_buf_(); - if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) - return aerr; - } - - if (!tx_buf_.empty()) { - // tx buf not empty, can't write now because then stream would be inconsistent - // Reserve space upfront to avoid multiple reallocations - tx_buf_.reserve(tx_buf_.size() + total_write_len); - for (int i = 0; i < iovcnt; i++) { - tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), - reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); - } - return APIError::OK; - } - - ssize_t sent = socket_->writev(iov, iovcnt); - if (is_would_block(sent)) { - // operation would block, add buffer to tx_buf - // Reserve space upfront to avoid multiple reallocations - tx_buf_.reserve(tx_buf_.size() + total_write_len); - for (int i = 0; i < iovcnt; i++) { - tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), - reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); - } - return APIError::OK; - } else if (sent == -1) { - // an error occurred - state_ = State::FAILED; - HELPER_LOG("Socket write failed with errno %d", errno); - return APIError::SOCKET_WRITE_FAILED; - } else if ((size_t) sent != total_write_len) { - // partially sent, add end to tx_buf - size_t remaining = total_write_len - sent; - // Reserve space upfront to avoid multiple reallocations - tx_buf_.reserve(tx_buf_.size() + remaining); - - size_t to_consume = sent; - for (int i = 0; i < iovcnt; i++) { - if (to_consume >= iov[i].iov_len) { - to_consume -= iov[i].iov_len; - } else { - tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base) + to_consume, - reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); - to_consume = 0; - } - } - return APIError::OK; - } - // fully sent - return APIError::OK; -} APIError APIPlaintextFrameHelper::close() { state_ = State::CLOSED; @@ -1067,6 +1011,11 @@ APIError APIPlaintextFrameHelper::shutdown(int how) { } return APIError::OK; } + +// Explicit template instantiation for Plaintext +template APIError APIFrameHelper::write_raw_( + const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf_, const std::string &info, + APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state); #endif // USE_API_PLAINTEXT } // namespace api diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 56d8bf1973..59f3cf7471 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -72,6 +72,12 @@ class APIFrameHelper { virtual APIError shutdown(int how) = 0; // Give this helper a name for logging virtual void set_log_info(std::string info) = 0; + + protected: + // Common implementation for writing raw data to socket + template + APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, + const std::string &info, StateEnum &state, StateEnum failed_state); }; #ifdef USE_API_NOISE @@ -103,7 +109,9 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError try_read_frame_(ParsedFrame *frame); APIError try_send_tx_buf_(); APIError write_frame_(const uint8_t *data, size_t len); - APIError write_raw_(const struct iovec *iov, int iovcnt); + inline APIError write_raw_(const struct iovec *iov, int iovcnt) { + return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED); + } APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const std::string &reason); @@ -164,7 +172,9 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError try_read_frame_(ParsedFrame *frame); APIError try_send_tx_buf_(); - APIError write_raw_(const struct iovec *iov, int iovcnt); + inline APIError write_raw_(const struct iovec *iov, int iovcnt) { + return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED); + } std::unique_ptr socket_; From 45d019a7e4a21b319e9cf06b3d865d5165ada221 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 19:18:19 -0500 Subject: [PATCH 020/193] Improve BLE Connection Reliability by Enabling Software Coexistence (#8683) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/esp32_ble_tracker/__init__.py | 6 +++++ .../esp32_ble_tracker/esp32_ble_tracker.cpp | 25 ++++++++++++++++--- .../esp32_ble_tracker/esp32_ble_tracker.h | 3 +++ .../esp32_ble_tracker/test.esp32-ard.yaml | 1 + .../esp32_ble_tracker/test.esp32-c3-ard.yaml | 1 + .../esp32_ble_tracker/test.esp32-c3-idf.yaml | 1 + .../esp32_ble_tracker/test.esp32-idf.yaml | 1 + 7 files changed, 35 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2c877367f8..a4425b9680 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -44,6 +44,7 @@ CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" CONF_ON_SCAN_END = "on_scan_end" +CONF_SOFTWARE_COEXISTENCE = "software_coexistence" DEFAULT_MAX_CONNECTIONS = 3 IDF_MAX_CONNECTIONS = 9 @@ -203,6 +204,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} ), + cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool, } ).extend(cv.COMPONENT_SCHEMA), ) @@ -310,6 +312,8 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + if config.get(CONF_SOFTWARE_COEXISTENCE): + add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True) # https://github.com/espressif/esp-idf/issues/4101 # https://github.com/espressif/esp-idf/issues/2503 # Match arduino CONFIG_BTU_TASK_STACK_SIZE @@ -331,6 +335,8 @@ async def to_code(config): cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") + if config.get(CONF_SOFTWARE_COEXISTENCE): + cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0dc0f58fa2..be45b177ff 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -21,6 +21,10 @@ #include "esphome/components/ota/ota_backend.h" #endif +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE +#include +#endif + #ifdef USE_ARDUINO #include #endif @@ -194,9 +198,17 @@ void ESP32BLETracker::loop() { https://github.com/espressif/esp-idf/issues/6688 */ - if (this->scanner_state_ == ScannerState::IDLE && this->scan_continuous_ && !connecting && !disconnecting && - !promote_to_connecting) { - this->start_scan_(false); // first = false + if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) { +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + if (this->coex_prefer_ble_) { + this->coex_prefer_ble_ = false; + ESP_LOGD(TAG, "Setting coexistence preference to balanced."); + esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default + } +#endif + if (this->scan_continuous_) { + this->start_scan_(false); // first = false + } } // If there is a discovered client and no connecting // clients and no clients using the scanner to search for @@ -213,6 +225,13 @@ void ESP32BLETracker::loop() { ESP_LOGD(TAG, "Promoting client to connect..."); // We only want to promote one client at a time. // once the scanner is fully stopped. +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); + if (!this->coex_prefer_ble_) { + this->coex_prefer_ble_ = true; + esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth + } +#endif client->set_state(ClientState::READY_TO_CONNECT); } break; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index ca2e53c343..2e45d9602c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -299,6 +299,9 @@ class ESP32BLETracker : public Component, int discovered_{0}; int searching_{0}; int disconnecting_{0}; +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + bool coex_prefer_ble_{false}; +#endif }; // NOLINTNEXTLINE diff --git a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml index 070fffd68b..3bfdb8773f 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml @@ -1,4 +1,5 @@ <<: !include common.yaml esp32_ble_tracker: + software_coexistence: true max_connections: 3 diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml index 070fffd68b..2e3c48117a 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml @@ -2,3 +2,4 @@ esp32_ble_tracker: max_connections: 3 + software_coexistence: false diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml index 5e09f5020e..b71896bad5 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml @@ -2,3 +2,4 @@ esp32_ble_tracker: max_connections: 9 + software_coexistence: false diff --git a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml index 5e09f5020e..1ffcfb9988 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml @@ -1,4 +1,5 @@ <<: !include common.yaml esp32_ble_tracker: + software_coexistence: true max_connections: 9 From 00f20c1e55698495fe2a9f49c17206e14ac0457f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 19:49:50 -0500 Subject: [PATCH 021/193] Optimize bluetooth_proxy memory copy and reduce reallocs (#8723) --- .../bluetooth_proxy/bluetooth_connection.cpp | 10 ++-- .../bluetooth_proxy/bluetooth_proxy.cpp | 60 +++++++++++++++++-- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index b63f7ccde9..3c5c2bd438 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -73,9 +73,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->read.handle; resp.data.reserve(param->read.value_len); - for (uint16_t i = 0; i < param->read.value_len; i++) { - resp.data.push_back(param->read.value[i]); - } + // Use bulk insert instead of individual push_backs + resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); break; } @@ -127,9 +126,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->notify.handle; resp.data.reserve(param->notify.value_len); - for (uint16_t i = 0; i < param->notify.value_len; i++) { - resp.data.push_back(param->notify.value[i]); - } + // Use bulk insert instead of individual push_backs + resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); break; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index e40f4e5dcc..9c8bd4009f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -56,6 +56,9 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p return false; api::BluetoothLERawAdvertisementsResponse resp; + // Pre-allocate the advertisements vector to avoid reallocations + resp.advertisements.reserve(count); + for (size_t i = 0; i < count; i++) { auto &result = advertisements[i]; api::BluetoothLERawAdvertisement adv; @@ -65,9 +68,8 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p uint8_t length = result.adv_data_len + result.scan_rsp_len; adv.data.reserve(length); - for (uint16_t i = 0; i < length; i++) { - adv.data.push_back(result.ble_adv[i]); - } + // Use a bulk insert instead of individual push_backs + adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]); resp.advertisements.push_back(std::move(adv)); @@ -85,21 +87,34 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi if (!device.get_name().empty()) resp.name = device.get_name(); resp.rssi = device.get_rssi(); - for (auto uuid : device.get_service_uuids()) { + + // Pre-allocate vectors based on known sizes + auto service_uuids = device.get_service_uuids(); + resp.service_uuids.reserve(service_uuids.size()); + for (auto uuid : service_uuids) { resp.service_uuids.push_back(uuid.to_string()); } - for (auto &data : device.get_service_datas()) { + + // Pre-allocate service data vector + auto service_datas = device.get_service_datas(); + resp.service_data.reserve(service_datas.size()); + for (auto &data : service_datas) { api::BluetoothServiceData service_data; service_data.uuid = data.uuid.to_string(); service_data.data.assign(data.data.begin(), data.data.end()); resp.service_data.push_back(std::move(service_data)); } - for (auto &data : device.get_manufacturer_datas()) { + + // Pre-allocate manufacturer data vector + auto manufacturer_datas = device.get_manufacturer_datas(); + resp.manufacturer_data.reserve(manufacturer_datas.size()); + for (auto &data : manufacturer_datas) { api::BluetoothServiceData manufacturer_data; manufacturer_data.uuid = data.uuid.to_string(); manufacturer_data.data.assign(data.data.begin(), data.data.end()); resp.manufacturer_data.push_back(std::move(manufacturer_data)); } + this->api_connection_->send_bluetooth_le_advertisement(resp); } @@ -161,11 +176,27 @@ void BluetoothProxy::loop() { } api::BluetoothGATTGetServicesResponse resp; resp.address = connection->get_address(); + resp.services.reserve(1); // Always one service per response in this implementation api::BluetoothGATTService service_resp; service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); service_resp.handle = service_result.start_handle; uint16_t char_offset = 0; esp_gattc_char_elem_t char_result; + // Get the number of characteristics directly with one call + uint16_t total_char_count = 0; + esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count( + connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC, + service_result.start_handle, service_result.end_handle, 0, &total_char_count); + + if (char_count_status == ESP_GATT_OK && total_char_count > 0) { + // Only reserve if we successfully got a count + service_resp.characteristics.reserve(total_char_count); + } else if (char_count_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(), + connection->address_str().c_str(), char_count_status); + } + + // Now process characteristics while (true) { // characteristics uint16_t char_count = 1; esp_gatt_status_t char_status = esp_ble_gattc_get_all_char( @@ -187,6 +218,23 @@ void BluetoothProxy::loop() { characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; char_offset++; + + // Get the number of descriptors directly with one call + uint16_t total_desc_count = 0; + esp_gatt_status_t desc_count_status = + esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR, + char_result.char_handle, service_result.end_handle, 0, &total_desc_count); + + if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { + // Only reserve if we successfully got a count + characteristic_resp.descriptors.reserve(total_desc_count); + } else if (desc_count_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", + connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle, + desc_count_status); + } + + // Now process descriptors uint16_t desc_offset = 0; esp_gattc_descr_elem_t desc_result; while (true) { // descriptors From e94e71ded80b5d6b5c943fce0f08d1f44a7ebe62 Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Thu, 8 May 2025 20:50:59 -0400 Subject: [PATCH 022/193] ATM90E32 Semi-automatic calibration & Status fields (#8529) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/atm90e32/__init__.py | 1 + esphome/components/atm90e32/atm90e32.cpp | 781 +++++++++++++----- esphome/components/atm90e32/atm90e32.h | 159 +++- esphome/components/atm90e32/atm90e32_reg.h | 6 +- .../components/atm90e32/button/__init__.py | 72 +- .../atm90e32/button/atm90e32_button.cpp | 67 +- .../atm90e32/button/atm90e32_button.h | 40 +- .../components/atm90e32/number/__init__.py | 130 +++ .../atm90e32/number/atm90e32_number.h | 16 + esphome/components/atm90e32/sensor.py | 25 +- .../atm90e32/text_sensor/__init__.py | 48 ++ tests/components/atm90e32/common.yaml | 84 +- 12 files changed, 1168 insertions(+), 261 deletions(-) create mode 100644 esphome/components/atm90e32/number/__init__.py create mode 100644 esphome/components/atm90e32/number/atm90e32_number.h create mode 100644 esphome/components/atm90e32/text_sensor/__init__.py diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py index 8ce95be489..766807872b 100644 --- a/esphome/components/atm90e32/__init__.py +++ b/esphome/components/atm90e32/__init__.py @@ -3,5 +3,6 @@ import esphome.codegen as cg CODEOWNERS = ["@circuitsetup", "@descipher"] atm90e32_ns = cg.esphome_ns.namespace("atm90e32") +ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component) CONF_ATM90E32_ID = "atm90e32_id" diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 43647b1855..f4f177587c 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,7 +1,7 @@ #include "atm90e32.h" -#include "atm90e32_reg.h" -#include "esphome/core/log.h" #include +#include +#include "esphome/core/log.h" namespace esphome { namespace atm90e32 { @@ -11,115 +11,84 @@ void ATM90E32Component::loop() { if (this->get_publish_interval_flag_()) { this->set_publish_interval_flag_(false); for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].voltage_sensor_ != nullptr) { + if (this->phase_[phase].voltage_sensor_ != nullptr) this->phase_[phase].voltage_ = this->get_phase_voltage_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].current_sensor_ != nullptr) { + + if (this->phase_[phase].current_sensor_ != nullptr) this->phase_[phase].current_ = this->get_phase_current_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_sensor_ != nullptr) { + + if (this->phase_[phase].power_sensor_ != nullptr) this->phase_[phase].active_power_ = this->get_phase_active_power_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_factor_sensor_ != nullptr) { + + if (this->phase_[phase].power_factor_sensor_ != nullptr) this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].reactive_power_sensor_ != nullptr) { + + if (this->phase_[phase].reactive_power_sensor_ != nullptr) this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { + + if (this->phase_[phase].apparent_power_sensor_ != nullptr) + this->phase_[phase].apparent_power_ = this->get_phase_apparent_power_(phase); + + if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { + + if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].phase_angle_sensor_ != nullptr) { + + if (this->phase_[phase].phase_angle_sensor_ != nullptr) this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { + + if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].peak_current_sensor_ != nullptr) { + + if (this->phase_[phase].peak_current_sensor_ != nullptr) this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase); - } - } - // After the local store in collected we can publish them trusting they are withing +-1 haardware sampling - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].voltage_sensor_ != nullptr) { + + // After the local store is collected we can publish them trusting they are within +-1 hardware sampling + if (this->phase_[phase].voltage_sensor_ != nullptr) this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].current_sensor_ != nullptr) { + + if (this->phase_[phase].current_sensor_ != nullptr) this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_sensor_ != nullptr) { + + if (this->phase_[phase].power_sensor_ != nullptr) this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_factor_sensor_ != nullptr) { + + if (this->phase_[phase].power_factor_sensor_ != nullptr) this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].reactive_power_sensor_ != nullptr) { + + if (this->phase_[phase].reactive_power_sensor_ != nullptr) this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { + + if (this->phase_[phase].apparent_power_sensor_ != nullptr) + this->phase_[phase].apparent_power_sensor_->publish_state(this->get_local_phase_apparent_power_(phase)); + if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { this->phase_[phase].forward_active_energy_sensor_->publish_state( this->get_local_phase_forward_active_energy_(phase)); } - } - for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { this->phase_[phase].reverse_active_energy_sensor_->publish_state( this->get_local_phase_reverse_active_energy_(phase)); } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].phase_angle_sensor_ != nullptr) { + + if (this->phase_[phase].phase_angle_sensor_ != nullptr) this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { this->phase_[phase].harmonic_active_power_sensor_->publish_state( this->get_local_phase_harmonic_active_power_(phase)); } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].peak_current_sensor_ != nullptr) { + + if (this->phase_[phase].peak_current_sensor_ != nullptr) this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase)); - } } - if (this->freq_sensor_ != nullptr) { + if (this->freq_sensor_ != nullptr) this->freq_sensor_->publish_state(this->get_frequency_()); - } - if (this->chip_temperature_sensor_ != nullptr) { + + if (this->chip_temperature_sensor_ != nullptr) this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); - } } } @@ -130,82 +99,30 @@ void ATM90E32Component::update() { } this->set_publish_interval_flag_(true); this->status_clear_warning(); -} -void ATM90E32Component::restore_calibrations_() { - if (enable_offset_calibration_) { - this->pref_.load(&this->offset_phase_); - } -}; - -void ATM90E32Component::run_offset_calibrations() { - // Run the calibrations and - // Setup voltage and current calibration offsets for PHASE A - this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); - this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); - this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset - // Setup voltage and current calibration offsets for PHASE B - this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); - this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); - this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset - // Setup voltage and current calibration offsets for PHASE C - this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); - this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); - this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset - this->pref_.save(&this->offset_phase_); - ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, - this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); - ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, - this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); -} - -void ATM90E32Component::clear_offset_calibrations() { - // Clear the calibrations and - this->offset_phase_[PHASEA].voltage_offset_ = 0; - this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEA].current_offset_ = 0; - this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset - this->offset_phase_[PHASEB].voltage_offset_ = 0; - this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEB].current_offset_ = 0; - this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset - this->offset_phase_[PHASEC].voltage_offset_ = 0; - this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEC].current_offset_ = 0; - this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset - this->pref_.save(&this->offset_phase_); - ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, - this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); - ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, - this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +#ifdef USE_TEXT_SENSOR + this->check_phase_status(); + this->check_over_current(); + this->check_freq_status(); +#endif } void ATM90E32Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - if (this->enable_offset_calibration_) { - uint32_t hash = fnv1_hash(App.get_friendly_name()); - this->pref_ = global_preferences->make_preference(hash, true); - this->restore_calibrations_(); - } + uint16_t mmode0 = 0x87; // 3P4W 50Hz + uint16_t high_thresh = 0; + uint16_t low_thresh = 0; + if (line_freq_ == 60) { mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz + // for freq threshold registers + high_thresh = 6300; // 63.00 Hz + low_thresh = 5700; // 57.00 Hz + } else { + high_thresh = 5300; // 53.00 Hz + low_thresh = 4700; // 47.00 Hz } if (current_phases_ == 2) { @@ -216,34 +133,84 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset delay(6); // Wait for the minimum 5ms + 1ms this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) { + if (!this->validate_spi_read_(0x55AA, "setup()")) { ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); this->mark_failed(); return; } this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering - this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0) + this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time (15:8) 255ms, Sag Period (7:0) 63ms this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) - this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config + this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // Zero crossing (ZX2, ZX1, ZX0) pin config this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels + this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh); // Frequency high threshold + this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh); // Frequency low threshold this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% - // Setup voltage and current gain for PHASE A - this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain - // Setup voltage and current gain for PHASE B - this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain - // Setup voltage and current gain for PHASE C - this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration + + if (this->enable_offset_calibration_) { + // Initialize flash storage for offset calibrations + uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary()); + this->offset_pref_ = global_preferences->make_preference(o_hash, true); + this->restore_offset_calibrations_(); + + // Initialize flash storage for power offset calibrations + uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary()); + this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); + this->restore_power_offset_calibrations_(); + } else { + ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values."); + for (uint8_t phase = 0; phase < 3; ++phase) { + this->write16_(this->voltage_offset_registers[phase], + static_cast(this->offset_phase_[phase].voltage_offset_)); + this->write16_(this->current_offset_registers[phase], + static_cast(this->offset_phase_[phase].current_offset_)); + this->write16_(this->power_offset_registers[phase], + static_cast(this->power_offset_phase_[phase].active_power_offset)); + this->write16_(this->reactive_power_offset_registers[phase], + static_cast(this->power_offset_phase_[phase].reactive_power_offset)); + } + } + + if (this->enable_gain_calibration_) { + // Initialize flash storage for gain calibration + uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary()); + this->gain_calibration_pref_ = global_preferences->make_preference(g_hash, true); + this->restore_gain_calibrations_(); + + if (this->using_saved_calibrations_) { + ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory."); + } else { + for (uint8_t phase = 0; phase < 3; ++phase) { + this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); + this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); + } + } + } else { + ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values."); + + for (uint8_t phase = 0; phase < 3; ++phase) { + this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); + this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); + } + } + + // Sag threshold (78%) + uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f); + // Overvoltage threshold (122%) + uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f); + + // Write to registers + this->write16_(ATM90E32_REGISTER_SAGTH, sagth); + this->write16_(ATM90E32_REGISTER_OVTH, ovth); + + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration } void ATM90E32Component::dump_config() { @@ -257,6 +224,7 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_); LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power A", this->phase_[PHASEA].apparent_power_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_); LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_); @@ -267,22 +235,24 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power B", this->phase_[PHASEB].apparent_power_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_); LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_); - LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_); - LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_); - LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_); + LOG_SENSOR(" ", "Harmonic Power B", this->phase_[PHASEB].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle B", this->phase_[PHASEB].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current B", this->phase_[PHASEB].peak_current_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power C", this->phase_[PHASEC].apparent_power_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_); LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_); - LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_); - LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_); - LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_); + LOG_SENSOR(" ", "Harmonic Power C", this->phase_[PHASEC].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle C", this->phase_[PHASEC].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } @@ -298,7 +268,7 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) { uint8_t data[2]; uint16_t output; this->enable(); - delay_microseconds_safe(10); + delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty this->write_byte(addrh); this->write_byte(addrl); this->read_array(data, 2); @@ -328,8 +298,7 @@ void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { this->write_byte16(a_register); this->write_byte16(val); this->disable(); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val) - ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val); + this->validate_spi_read_(val, "write16()"); } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } @@ -340,6 +309,8 @@ float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return t float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; } +float ATM90E32Component::get_local_phase_apparent_power_(uint8_t phase) { return this->phase_[phase].apparent_power_; } + float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; } float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) { @@ -360,8 +331,7 @@ float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return t float ATM90E32Component::get_phase_voltage_(uint8_t phase) { const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) - ESP_LOGW(TAG, "SPI URMS voltage register read error."); + this->validate_spi_read_(voltage, "get_phase_voltage()"); return (float) voltage / 100; } @@ -371,8 +341,7 @@ float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) { uint16_t voltage = 0; for (uint8_t i = 0; i < reads; i++) { voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) - ESP_LOGW(TAG, "SPI URMS voltage register read error."); + this->validate_spi_read_(voltage, "get_phase_voltage_avg_()"); accumulation += voltage; } voltage = accumulation / reads; @@ -386,8 +355,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) { uint16_t current = 0; for (uint8_t i = 0; i < reads; i++) { current = this->read16_(ATM90E32_REGISTER_IRMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) - ESP_LOGW(TAG, "SPI IRMS current register read error."); + this->validate_spi_read_(current, "get_phase_current_avg_()"); accumulation += current; } current = accumulation / reads; @@ -397,8 +365,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) { float ATM90E32Component::get_phase_current_(uint8_t phase) { const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) - ESP_LOGW(TAG, "SPI IRMS current register read error."); + this->validate_spi_read_(current, "get_phase_current_()"); return (float) current / 1000; } @@ -412,11 +379,15 @@ float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) { return val * 0.00032f; } +float ATM90E32Component::get_phase_apparent_power_(uint8_t phase) { + const int val = this->read32_(ATM90E32_REGISTER_SMEAN + phase, ATM90E32_REGISTER_SMEANLSB + phase); + return val * 0.00032f; +} + float ATM90E32Component::get_phase_power_factor_(uint8_t phase) { - const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor) - ESP_LOGW(TAG, "SPI power factor read error."); - return (float) powerfactor / 1000; + uint16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); // unsigned to compare to lastspidata + this->validate_spi_read_(powerfactor, "get_phase_power_factor_()"); + return (float) ((int16_t) powerfactor) / 1000; // make it signed again } float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { @@ -426,17 +397,19 @@ float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { } else { this->phase_[phase].cumulative_forward_active_energy_ = val; } - return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200); + // 0.01CF resolution = 0.003125 Wh per count + return ((float) this->phase_[phase].cumulative_forward_active_energy_ * (10.0f / 3200.0f)); } float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) { - const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY); + const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY + phase); if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) { this->phase_[phase].cumulative_reverse_active_energy_ += val; } else { this->phase_[phase].cumulative_reverse_active_energy_ = val; } - return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200); + // 0.01CF resolution = 0.003125 Wh per count + return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * (10.0f / 3200.0f)); } float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { @@ -446,15 +419,15 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { float ATM90E32Component::get_phase_angle_(uint8_t phase) { uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; - return (float) (val > 180) ? val - 360.0 : val; + return (val > 180) ? (float) (val - 360.0f) : (float) val; } float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase); if (!this->peak_current_signed_) - val = abs(val); + val = std::abs(val); // phase register * phase current gain value / 1000 * 2^13 - return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0); + return (val * this->phase_[phase].ct_gain_ / 8192000.0); } float ATM90E32Component::get_frequency_() { @@ -467,29 +440,433 @@ float ATM90E32Component::get_chip_temperature_() { return (float) ctemp; } -uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) { - const uint8_t num_reads = 5; - uint64_t total_value = 0; - for (int i = 0; i < num_reads; ++i) { - const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase); - total_value += measurement_value; +void ATM90E32Component::run_gain_calibrations() { + if (!this->enable_gain_calibration_) { + ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true"); + return; } - const uint32_t average_value = total_value / num_reads; - const uint32_t shifted_value = average_value >> 7; - const uint32_t voltage_offset = ~shifted_value + 1; - return voltage_offset & 0xFFFF; // Take the lower 16 bits + + float ref_voltages[3] = { + this->get_reference_voltage(0), + this->get_reference_voltage(1), + this->get_reference_voltage(2), + }; + float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1), + this->get_reference_current(2)}; + + ESP_LOGI(TAG, "[CALIBRATION] "); + ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration ========================="); + ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); + ESP_LOGI(TAG, + "[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |"); + ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); + + for (uint8_t phase = 0; phase < 3; phase++) { + float measured_voltage = this->get_phase_voltage_avg_(phase); + float measured_current = this->get_phase_current_avg_(phase); + + float ref_voltage = ref_voltages[phase]; + float ref_current = ref_currents[phase]; + + uint16_t current_voltage_gain = this->read16_(voltage_gain_registers[phase]); + uint16_t current_current_gain = this->read16_(current_gain_registers[phase]); + + bool did_voltage = false; + bool did_current = false; + + // Voltage calibration + if (ref_voltage <= 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.", + phase_labels[phase]); + } else if (measured_voltage == 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.", + phase_labels[phase]); + } else { + uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); + if (new_voltage_gain == 0) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", + phase_labels[phase]); + } else { + if (new_voltage_gain >= 65535) { + ESP_LOGW( + TAG, + "[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.", + phase_labels[phase]); + new_voltage_gain = 65535; + } + this->gain_phase_[phase].voltage_gain = static_cast(new_voltage_gain); + did_voltage = true; + } + } + + // Current calibration + if (ref_current == 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.", + phase_labels[phase]); + } else if (measured_current == 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.", + phase_labels[phase]); + } else { + uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); + if (new_current_gain == 0) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.", + phase_labels[phase]); + } else { + if (new_current_gain >= 65535) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", + phase_labels[phase]); + new_current_gain = 65535; + } + this->gain_phase_[phase].current_gain = static_cast(new_current_gain); + did_current = true; + } + } + + // Final row output + ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", + 'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain, + did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain, + did_current ? this->gain_phase_[phase].current_gain : current_current_gain); + } + + ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n"); + + this->save_gain_calibration_to_memory_(); + this->write_gains_to_registers_(); + this->verify_gain_writes_(); } -uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) { +void ATM90E32Component::save_gain_calibration_to_memory_() { + bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + if (success) { + this->using_saved_calibrations_ = true; + ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory."); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!"); + } +} + +void ATM90E32Component::run_offset_calibrations() { + if (!this->enable_offset_calibration_) { + ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true"); + return; + } + + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t voltage_offset = calibrate_offset(phase, true); + int16_t current_offset = calibrate_offset(phase, false); + + this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset, + current_offset); + } + + this->offset_pref_.save(&this->offset_phase_); // Save to flash +} + +void ATM90E32Component::run_power_offset_calibrations() { + if (!this->enable_offset_calibration_) { + ESP_LOGW( + TAG, + "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true"); + return; + } + + for (uint8_t phase = 0; phase < 3; ++phase) { + int16_t active_offset = calibrate_power_offset(phase, false); + int16_t reactive_offset = calibrate_power_offset(phase, true); + + this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, + active_offset, reactive_offset); + } + + this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash +} + +void ATM90E32Component::write_gains_to_registers_() { + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); + + for (int phase = 0; phase < 3; phase++) { + this->write16_(voltage_gain_registers[phase], this->gain_phase_[phase].voltage_gain); + this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain); + } + + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); +} + +void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) { + // Save to runtime + this->offset_phase_[phase].voltage_offset_ = voltage_offset; + this->phase_[phase].voltage_offset_ = voltage_offset; + + // Save to flash-storable struct + this->offset_phase_[phase].current_offset_ = current_offset; + this->phase_[phase].current_offset_ = current_offset; + + // Write to registers + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); + this->write16_(voltage_offset_registers[phase], static_cast(voltage_offset)); + this->write16_(current_offset_registers[phase], static_cast(current_offset)); + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); +} + +void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) { + // Save to runtime + this->phase_[phase].active_power_offset_ = p_offset; + this->phase_[phase].reactive_power_offset_ = q_offset; + + // Save to flash-storable struct + this->power_offset_phase_[phase].active_power_offset = p_offset; + this->power_offset_phase_[phase].reactive_power_offset = q_offset; + + // Write to registers + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); + this->write16_(this->power_offset_registers[phase], static_cast(p_offset)); + this->write16_(this->reactive_power_offset_registers[phase], static_cast(q_offset)); + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); +} + +void ATM90E32Component::restore_gain_calibrations_() { + if (this->gain_calibration_pref_.load(&this->gain_phase_)) { + ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:"); + + for (uint8_t phase = 0; phase < 3; phase++) { + uint16_t v_gain = this->gain_phase_[phase].voltage_gain; + uint16_t i_gain = this->gain_phase_[phase].current_gain; + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain); + } + + this->write_gains_to_registers_(); + + if (this->verify_gain_writes_()) { + this->using_saved_calibrations_ = true; + ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully."); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly."); + } + } else { + this->using_saved_calibrations_ = false; + ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values."); + } +} + +void ATM90E32Component::restore_offset_calibrations_() { + if (this->offset_pref_.load(&this->offset_phase_)) { + ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory."); + + for (uint8_t phase = 0; phase < 3; phase++) { + auto &offset = this->offset_phase_[phase]; + write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_); + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase, + offset.voltage_offset_, offset.current_offset_); + } + } else { + ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values."); + } +} + +void ATM90E32Component::restore_power_offset_calibrations_() { + if (this->power_offset_pref_.load(&this->power_offset_phase_)) { + ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory."); + + for (uint8_t phase = 0; phase < 3; ++phase) { + auto &offset = this->power_offset_phase_[phase]; + write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset); + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, + offset.active_power_offset, offset.reactive_power_offset); + } + } else { + ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values."); + } +} + +void ATM90E32Component::clear_gain_calibrations() { + ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values..."); + + for (int phase = 0; phase < 3; phase++) { + gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_; + gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_; + } + + bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + this->using_saved_calibrations_ = false; + + if (success) { + ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:"); + for (int phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, + gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain); + } + } else { + ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!"); + } + + this->write_gains_to_registers_(); // Apply them to the chip immediately +} + +void ATM90E32Component::clear_offset_calibrations() { + for (uint8_t phase = 0; phase < 3; phase++) { + this->write_offsets_to_registers_(phase, 0, 0); + } + + this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory + + ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared."); +} + +void ATM90E32Component::clear_power_offset_calibrations() { + for (uint8_t phase = 0; phase < 3; phase++) { + this->write_power_offsets_to_registers_(phase, 0, 0); + } + + this->power_offset_pref_.save(&this->power_offset_phase_); + + ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared."); +} + +int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { const uint8_t num_reads = 5; uint64_t total_value = 0; - for (int i = 0; i < num_reads; ++i) { - const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); - total_value += measurement_value; + + for (uint8_t i = 0; i < num_reads; ++i) { + uint32_t reading = voltage ? this->read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase) + : this->read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); + total_value += reading; } + const uint32_t average_value = total_value / num_reads; - const uint32_t current_offset = ~average_value + 1; - return current_offset & 0xFFFF; // Take the lower 16 bits + const uint32_t shifted = average_value >> 7; + const uint32_t offset = ~shifted + 1; + return static_cast(offset); // Takes lower 16 bits +} + +int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) { + const uint8_t num_reads = 5; + uint64_t total_value = 0; + + for (uint8_t i = 0; i < num_reads; ++i) { + uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) + : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); + total_value += reading; + } + + const uint32_t average_value = total_value / num_reads; + const uint32_t power_offset = ~average_value + 1; + return static_cast(power_offset); // Takes the lower 16 bits +} + +bool ATM90E32Component::verify_gain_writes_() { + bool success = true; + for (uint8_t phase = 0; phase < 3; phase++) { + uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); + uint16_t read_current = this->read16_(current_gain_registers[phase]); + + if (read_voltage != this->gain_phase_[phase].voltage_gain || + read_current != this->gain_phase_[phase].current_gain) { + ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]); + success = false; + } + } + return success; // Return true if all writes were successful, false otherwise +} + +#ifdef USE_TEXT_SENSOR +void ATM90E32Component::check_phase_status() { + uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0); + uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1); + + for (int phase = 0; phase < 3; phase++) { + std::string status; + + if (state0 & over_voltage_flags[phase]) + status += "Over Voltage; "; + if (state1 & voltage_sag_flags[phase]) + status += "Voltage Sag; "; + if (state1 & phase_loss_flags[phase]) + status += "Phase Loss; "; + + auto *sensor = this->phase_status_text_sensor_[phase]; + const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase"; + if (!status.empty()) { + status.pop_back(); // remove space + status.pop_back(); // remove semicolon + ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str()); + if (sensor != nullptr) + sensor->publish_state(status); + } else { + if (sensor != nullptr) + sensor->publish_state("Okay"); + } + } +} + +void ATM90E32Component::check_freq_status() { + uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1); + + std::string freq_status; + + if (state1 & ATM90E32_STATUS_S1_FREQHIST) { + freq_status = "HIGH"; + } else if (state1 & ATM90E32_STATUS_S1_FREQLOST) { + freq_status = "LOW"; + } else { + freq_status = "Normal"; + } + ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); + + if (this->freq_status_text_sensor_ != nullptr) { + this->freq_status_text_sensor_->publish_state(freq_status); + } +} + +void ATM90E32Component::check_over_current() { + constexpr float max_current_threshold = 65.53f; + + for (uint8_t phase = 0; phase < 3; phase++) { + float current_val = + this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f; + + if (current_val > max_current_threshold) { + ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val); + ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2"); + if (this->phase_status_text_sensor_[phase] != nullptr) { + this->phase_status_text_sensor_[phase]->publish_state("Over Current; "); + } + } + } +} +#endif + +uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) { + // this assumes that 60Hz electrical systems use 120V mains, + // which is usually, but not always the case + float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; + float target_voltage = nominal_voltage * multiplier; + + float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V + float divider = (2.0f * ugain) / 32768.0f; + + float threshold = peak_01v / divider; + + return static_cast(threshold); +} + +bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) { + uint16_t last = this->read16_(ATM90E32_REGISTER_LASTSPIDATA); + if (last != expected) { + if (context != nullptr) { + ESP_LOGW(TAG, "[%s] SPI read mismatch: expected 0x%04X, got 0x%04X", context, expected, last); + } else { + ESP_LOGW(TAG, "SPI read mismatch: expected 0x%04X, got 0x%04X", expected, last); + } + return false; + } + return true; } } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 35c61d1e05..0703c40ae0 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -1,5 +1,6 @@ #pragma once +#include #include "atm90e32_reg.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" @@ -18,6 +19,26 @@ class ATM90E32Component : public PollingComponent, static const uint8_t PHASEA = 0; static const uint8_t PHASEB = 1; static const uint8_t PHASEC = 2; + const char *phase_labels[3] = {"A", "B", "C"}; + // these registers are not sucessive, so we can't just do 'base + phase' + const uint16_t voltage_gain_registers[3] = {ATM90E32_REGISTER_UGAINA, ATM90E32_REGISTER_UGAINB, + ATM90E32_REGISTER_UGAINC}; + const uint16_t current_gain_registers[3] = {ATM90E32_REGISTER_IGAINA, ATM90E32_REGISTER_IGAINB, + ATM90E32_REGISTER_IGAINC}; + const uint16_t voltage_offset_registers[3] = {ATM90E32_REGISTER_UOFFSETA, ATM90E32_REGISTER_UOFFSETB, + ATM90E32_REGISTER_UOFFSETC}; + const uint16_t current_offset_registers[3] = {ATM90E32_REGISTER_IOFFSETA, ATM90E32_REGISTER_IOFFSETB, + ATM90E32_REGISTER_IOFFSETC}; + const uint16_t power_offset_registers[3] = {ATM90E32_REGISTER_POFFSETA, ATM90E32_REGISTER_POFFSETB, + ATM90E32_REGISTER_POFFSETC}; + const uint16_t reactive_power_offset_registers[3] = {ATM90E32_REGISTER_QOFFSETA, ATM90E32_REGISTER_QOFFSETB, + ATM90E32_REGISTER_QOFFSETC}; + const uint16_t over_voltage_flags[3] = {ATM90E32_STATUS_S0_OVPHASEAST, ATM90E32_STATUS_S0_OVPHASEBST, + ATM90E32_STATUS_S0_OVPHASECST}; + const uint16_t voltage_sag_flags[3] = {ATM90E32_STATUS_S1_SAGPHASEAST, ATM90E32_STATUS_S1_SAGPHASEBST, + ATM90E32_STATUS_S1_SAGPHASECST}; + const uint16_t phase_loss_flags[3] = {ATM90E32_STATUS_S1_PHASELOSSAST, ATM90E32_STATUS_S1_PHASELOSSBST, + ATM90E32_STATUS_S1_PHASELOSSCST}; void loop() override; void setup() override; void dump_config() override; @@ -42,6 +63,14 @@ class ATM90E32Component : public PollingComponent, void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } + void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; } + void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; } + void set_active_power_offset(uint8_t phase, int16_t offset) { + this->power_offset_phase_[phase].active_power_offset = offset; + } + void set_reactive_power_offset(uint8_t phase, int16_t offset) { + this->power_offset_phase_[phase].reactive_power_offset = offset; + } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { @@ -51,53 +80,104 @@ class ATM90E32Component : public PollingComponent, void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } void run_offset_calibrations(); + void run_power_offset_calibrations(); void clear_offset_calibrations(); + void clear_power_offset_calibrations(); + void clear_gain_calibrations(); void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } - uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); - uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); + void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; } + int16_t calibrate_offset(uint8_t phase, bool voltage); + int16_t calibrate_power_offset(uint8_t phase, bool reactive); + void run_gain_calibrations(); +#ifdef USE_NUMBER + void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; } + void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; } +#endif + float get_reference_voltage(uint8_t phase) { +#ifdef USE_NUMBER + return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage +#else + return 120.0; // Default voltage +#endif + } + float get_reference_current(uint8_t phase) { +#ifdef USE_NUMBER + return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current +#else + return 5.0f; // Default current +#endif + } + bool using_saved_calibrations_ = false; // Track if stored calibrations are being used +#ifdef USE_TEXT_SENSOR + void check_phase_status(); + void check_freq_status(); + void check_over_current(); + void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) { + this->phase_status_text_sensor_[phase] = sensor; + } + void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; } +#endif + uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier); int32_t last_periodic_millis = millis(); protected: +#ifdef USE_NUMBER + number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr}; + number::Number *ref_currents_[3]{nullptr, nullptr, nullptr}; +#endif uint16_t read16_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); void write16_(uint16_t a_register, uint16_t val); - float get_local_phase_voltage_(uint8_t /*phase*/); - float get_local_phase_current_(uint8_t /*phase*/); - float get_local_phase_active_power_(uint8_t /*phase*/); - float get_local_phase_reactive_power_(uint8_t /*phase*/); - float get_local_phase_power_factor_(uint8_t /*phase*/); - float get_local_phase_forward_active_energy_(uint8_t /*phase*/); - float get_local_phase_reverse_active_energy_(uint8_t /*phase*/); - float get_local_phase_angle_(uint8_t /*phase*/); - float get_local_phase_harmonic_active_power_(uint8_t /*phase*/); - float get_local_phase_peak_current_(uint8_t /*phase*/); - float get_phase_voltage_(uint8_t /*phase*/); - float get_phase_voltage_avg_(uint8_t /*phase*/); - float get_phase_current_(uint8_t /*phase*/); - float get_phase_current_avg_(uint8_t /*phase*/); - float get_phase_active_power_(uint8_t /*phase*/); - float get_phase_reactive_power_(uint8_t /*phase*/); - float get_phase_power_factor_(uint8_t /*phase*/); - float get_phase_forward_active_energy_(uint8_t /*phase*/); - float get_phase_reverse_active_energy_(uint8_t /*phase*/); - float get_phase_angle_(uint8_t /*phase*/); - float get_phase_harmonic_active_power_(uint8_t /*phase*/); - float get_phase_peak_current_(uint8_t /*phase*/); + float get_local_phase_voltage_(uint8_t phase); + float get_local_phase_current_(uint8_t phase); + float get_local_phase_active_power_(uint8_t phase); + float get_local_phase_reactive_power_(uint8_t phase); + float get_local_phase_apparent_power_(uint8_t phase); + float get_local_phase_power_factor_(uint8_t phase); + float get_local_phase_forward_active_energy_(uint8_t phase); + float get_local_phase_reverse_active_energy_(uint8_t phase); + float get_local_phase_angle_(uint8_t phase); + float get_local_phase_harmonic_active_power_(uint8_t phase); + float get_local_phase_peak_current_(uint8_t phase); + float get_phase_voltage_(uint8_t phase); + float get_phase_voltage_avg_(uint8_t phase); + float get_phase_current_(uint8_t phase); + float get_phase_current_avg_(uint8_t phase); + float get_phase_active_power_(uint8_t phase); + float get_phase_reactive_power_(uint8_t phase); + float get_phase_apparent_power_(uint8_t phase); + float get_phase_power_factor_(uint8_t phase); + float get_phase_forward_active_energy_(uint8_t phase); + float get_phase_reverse_active_energy_(uint8_t phase); + float get_phase_angle_(uint8_t phase); + float get_phase_harmonic_active_power_(uint8_t phase); + float get_phase_peak_current_(uint8_t phase); float get_frequency_(); float get_chip_temperature_(); bool get_publish_interval_flag_() { return publish_interval_flag_; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; - void restore_calibrations_(); + void restore_offset_calibrations_(); + void restore_power_offset_calibrations_(); + void restore_gain_calibrations_(); + void save_gain_calibration_to_memory_(); + void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); + void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); + void write_gains_to_registers_(); + bool verify_gain_writes_(); + bool validate_spi_read_(uint16_t expected, const char *context = nullptr); struct ATM90E32Phase { uint16_t voltage_gain_{0}; uint16_t ct_gain_{0}; - uint16_t voltage_offset_{0}; - uint16_t current_offset_{0}; + int16_t voltage_offset_{0}; + int16_t current_offset_{0}; + int16_t active_power_offset_{0}; + int16_t reactive_power_offset_{0}; float voltage_{0}; float current_{0}; float active_power_{0}; float reactive_power_{0}; + float apparent_power_{0}; float power_factor_{0}; float forward_active_energy_{0}; float reverse_active_energy_{0}; @@ -119,14 +199,30 @@ class ATM90E32Component : public PollingComponent, uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; - struct Calibration { - uint16_t voltage_offset_{0}; - uint16_t current_offset_{0}; + struct OffsetCalibration { + int16_t voltage_offset_{0}; + int16_t current_offset_{0}; } offset_phase_[3]; - ESPPreferenceObject pref_; + struct PowerOffsetCalibration { + int16_t active_power_offset{0}; + int16_t reactive_power_offset{0}; + } power_offset_phase_[3]; + + struct GainCalibration { + uint16_t voltage_gain{1}; + uint16_t current_gain{1}; + } gain_phase_[3]; + + ESPPreferenceObject offset_pref_; + ESPPreferenceObject power_offset_pref_; + ESPPreferenceObject gain_calibration_pref_; sensor::Sensor *freq_sensor_{nullptr}; +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr}; + text_sensor::TextSensor *freq_status_text_sensor_{nullptr}; +#endif sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; @@ -134,6 +230,7 @@ class ATM90E32Component : public PollingComponent, bool publish_interval_flag_{false}; bool peak_current_signed_{false}; bool enable_offset_calibration_{false}; + bool enable_gain_calibration_{false}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index 954fb42e79..86c2d64569 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -176,16 +176,17 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E /* POWER & P.F. REGISTERS */ static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P) -static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P) +static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Active Power Reg Base (P) static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P) static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q) -static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q) +static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Reactive Power Reg Base (Q) static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q) static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S) +static const uint16_t ATM90E32_REGISTER_SMEAN = 0xB9; // Apparent Mean Power Base (S) static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S) @@ -206,6 +207,7 @@ static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A Rea static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power) static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power) static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power) +static const uint16_t ATM90E32_REGISTER_SMEANLSB = 0xC9; // Lower Word Reg Base (Apparent Power) static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power) static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power) static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power) diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py index 931346b386..19f62ccfbd 100644 --- a/esphome/components/atm90e32/button/__init__.py +++ b/esphome/components/atm90e32/button/__init__.py @@ -1,43 +1,95 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv -from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE +from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE from .. import atm90e32_ns from ..sensor import ATM90E32Component +CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration" +CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration" CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" +CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration" +CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration" -ATM90E32CalibrationButton = atm90e32_ns.class_( - "ATM90E32CalibrationButton", - button.Button, +ATM90E32GainCalibrationButton = atm90e32_ns.class_( + "ATM90E32GainCalibrationButton", button.Button ) -ATM90E32ClearCalibrationButton = atm90e32_ns.class_( - "ATM90E32ClearCalibrationButton", - button.Button, +ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearGainCalibrationButton", button.Button +) +ATM90E32OffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32OffsetCalibrationButton", button.Button +) +ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearOffsetCalibrationButton", button.Button +) +ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32PowerOffsetCalibrationButton", button.Button +) +ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearPowerOffsetCalibrationButton", button.Button ) CONFIG_SCHEMA = { cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema( + ATM90E32GainCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:scale-balance", + ), + cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema( + ATM90E32ClearGainCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:delete", + ), cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( - ATM90E32CalibrationButton, + ATM90E32OffsetCalibrationButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_SCALE, ), cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( - ATM90E32ClearCalibrationButton, + ATM90E32ClearOffsetCalibrationButton, entity_category=ENTITY_CATEGORY_CONFIG, - icon=ICON_CHIP, + icon="mdi:delete", + ), + cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema( + ATM90E32PowerOffsetCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema( + ATM90E32ClearPowerOffsetCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:delete", ), } async def to_code(config): parent = await cg.get_variable(config[CONF_ID]) + + if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION): + b = await button.new_button(run_gain) + await cg.register_parented(b, parent) + + if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION): + b = await button.new_button(clear_gain) + await cg.register_parented(b, parent) + if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): b = await button.new_button(run_offset) await cg.register_parented(b, parent) + if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): b = await button.new_button(clear_offset) await cg.register_parented(b, parent) + + if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION): + b = await button.new_button(run_power) + await cg.register_parented(b, parent) + + if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION): + b = await button.new_button(clear_power) + await cg.register_parented(b, parent) diff --git a/esphome/components/atm90e32/button/atm90e32_button.cpp b/esphome/components/atm90e32/button/atm90e32_button.cpp index 00715b61dd..a89f071997 100644 --- a/esphome/components/atm90e32/button/atm90e32_button.cpp +++ b/esphome/components/atm90e32/button/atm90e32_button.cpp @@ -1,4 +1,5 @@ #include "atm90e32_button.h" +#include "esphome/core/component.h" #include "esphome/core/log.h" namespace esphome { @@ -6,15 +7,73 @@ namespace atm90e32 { static const char *const TAG = "atm90e32.button"; -void ATM90E32CalibrationButton::press_action() { - ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); +void ATM90E32GainCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + ESP_LOGI(TAG, + "[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values"); + this->parent_->run_gain_calibrations(); +} + +void ATM90E32ClearGainCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + this->parent_->clear_gain_calibrations(); +} + +void ATM90E32OffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**"); + ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save " + "these values"); this->parent_->run_offset_calibrations(); } -void ATM90E32ClearCalibrationButton::press_action() { - ESP_LOGI(TAG, "Offset calibrations cleared."); +void ATM90E32ClearOffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); this->parent_->clear_offset_calibrations(); } +void ATM90E32PowerOffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**"); + ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config " + "file to save these values"); + this->parent_->run_power_offset_calibrations(); +} + +void ATM90E32ClearPowerOffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + this->parent_->clear_power_offset_calibrations(); +} + } // namespace atm90e32 } // namespace esphome diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h index 0617099457..2449581531 100644 --- a/esphome/components/atm90e32/button/atm90e32_button.h +++ b/esphome/components/atm90e32/button/atm90e32_button.h @@ -7,17 +7,49 @@ namespace esphome { namespace atm90e32 { -class ATM90E32CalibrationButton : public button::Button, public Parented { +class ATM90E32GainCalibrationButton : public button::Button, public Parented { public: - ATM90E32CalibrationButton() = default; + ATM90E32GainCalibrationButton() = default; protected: void press_action() override; }; -class ATM90E32ClearCalibrationButton : public button::Button, public Parented { +class ATM90E32ClearGainCalibrationButton : public button::Button, public Parented { public: - ATM90E32ClearCalibrationButton() = default; + ATM90E32ClearGainCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32OffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32OffsetCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearOffsetCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32PowerOffsetCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearPowerOffsetCalibrationButton() = default; protected: void press_action() override; diff --git a/esphome/components/atm90e32/number/__init__.py b/esphome/components/atm90e32/number/__init__.py new file mode 100644 index 0000000000..848680b875 --- /dev/null +++ b/esphome/components/atm90e32/number/__init__.py @@ -0,0 +1,130 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_PHASE_A, + CONF_PHASE_B, + CONF_PHASE_C, + CONF_REFERENCE_VOLTAGE, + CONF_STEP, + ENTITY_CATEGORY_CONFIG, + UNIT_AMPERE, + UNIT_VOLT, +) + +from .. import atm90e32_ns +from ..sensor import ATM90E32Component + +ATM90E32Number = atm90e32_ns.class_( + "ATM90E32Number", number.Number, cg.Parented.template(ATM90E32Component) +) + +CONF_REFERENCE_CURRENT = "reference_current" +PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C] + + +REFERENCE_VOLTAGE_PHASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MODE, default="box"): cv.string, + cv.Optional(CONF_MIN_VALUE, default=100.0): cv.float_, + cv.Optional(CONF_MAX_VALUE, default=260.0): cv.float_, + cv.Optional(CONF_STEP, default=0.1): cv.float_, + } + ).extend( + number.number_schema( + class_=ATM90E32Number, + unit_of_measurement=UNIT_VOLT, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:power-plug", + ) + ) +) + + +REFERENCE_CURRENT_PHASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MODE, default="box"): cv.string, + cv.Optional(CONF_MIN_VALUE, default=1.0): cv.float_, + cv.Optional(CONF_MAX_VALUE, default=200.0): cv.float_, + cv.Optional(CONF_STEP, default=0.1): cv.float_, + } + ).extend( + number.number_schema( + class_=ATM90E32Number, + unit_of_measurement=UNIT_AMPERE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:home-lightning-bolt", + ) + ) +) + + +REFERENCE_VOLTAGE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PHASE_A): REFERENCE_VOLTAGE_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): REFERENCE_VOLTAGE_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): REFERENCE_VOLTAGE_PHASE_SCHEMA, + } +) + +REFERENCE_CURRENT_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PHASE_A): REFERENCE_CURRENT_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): REFERENCE_CURRENT_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): REFERENCE_CURRENT_PHASE_SCHEMA, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_REFERENCE_VOLTAGE): REFERENCE_VOLTAGE_SCHEMA, + cv.Optional(CONF_REFERENCE_CURRENT): REFERENCE_CURRENT_SCHEMA, + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE): + voltage_objs = [None, None, None] + + for i, key in enumerate(PHASE_KEYS): + if validated := voltage_cfg.get(key): + obj = await number.new_number( + validated, + min_value=validated["min_value"], + max_value=validated["max_value"], + step=validated["step"], + ) + await cg.register_parented(obj, parent) + voltage_objs[i] = obj + + # Inherit from A → B/C if only A defined + if voltage_objs[0] is not None: + for i in range(3): + if voltage_objs[i] is None: + voltage_objs[i] = voltage_objs[0] + + for i, obj in enumerate(voltage_objs): + if obj is not None: + cg.add(parent.set_reference_voltage(i, obj)) + + if current_cfg := config.get(CONF_REFERENCE_CURRENT): + for i, key in enumerate(PHASE_KEYS): + if validated := current_cfg.get(key): + obj = await number.new_number( + validated, + min_value=validated["min_value"], + max_value=validated["max_value"], + step=validated["step"], + ) + await cg.register_parented(obj, parent) + cg.add(parent.set_reference_current(i, obj)) diff --git a/esphome/components/atm90e32/number/atm90e32_number.h b/esphome/components/atm90e32/number/atm90e32_number.h new file mode 100644 index 0000000000..9b6129b26d --- /dev/null +++ b/esphome/components/atm90e32/number/atm90e32_number.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/atm90e32/atm90e32.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace atm90e32 { + +class ATM90E32Number : public number::Number, public Parented { + public: + void control(float value) override { this->publish_state(value); } +}; + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 0dc3bfdc4f..7cdbd69f56 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -33,6 +33,7 @@ from esphome.const import ( UNIT_DEGREES, UNIT_HERTZ, UNIT_VOLT, + UNIT_VOLT_AMPS, UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT, UNIT_WATT_HOURS, @@ -45,10 +46,17 @@ CONF_GAIN_PGA = "gain_pga" CONF_CURRENT_PHASES = "current_phases" CONF_GAIN_VOLTAGE = "gain_voltage" CONF_GAIN_CT = "gain_ct" +CONF_OFFSET_VOLTAGE = "offset_voltage" +CONF_OFFSET_CURRENT = "offset_current" +CONF_OFFSET_ACTIVE_POWER = "offset_active_power" +CONF_OFFSET_REACTIVE_POWER = "offset_reactive_power" CONF_HARMONIC_POWER = "harmonic_power" CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" +CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration" +CONF_PHASE_STATUS = "phase_status" +CONF_FREQUENCY_STATUS = "frequency_status" UNIT_DEG = "degrees" LINE_FREQS = { "50HZ": 50, @@ -92,10 +100,11 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, + unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -137,6 +146,10 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, + cv.Optional(CONF_OFFSET_VOLTAGE, default=0): cv.int_, + cv.Optional(CONF_OFFSET_CURRENT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_ACTIVE_POWER, default=0): cv.int_, + cv.Optional(CONF_OFFSET_REACTIVE_POWER, default=0): cv.int_, } ) @@ -164,9 +177,10 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( CURRENT_PHASES, upper=True ), - cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), + cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -185,6 +199,10 @@ async def to_code(config): conf = config[phase] cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) + cg.add(var.set_voltage_offset(i, conf[CONF_OFFSET_VOLTAGE])) + cg.add(var.set_current_offset(i, conf[CONF_OFFSET_CURRENT])) + cg.add(var.set_active_power_offset(i, conf[CONF_OFFSET_ACTIVE_POWER])) + cg.add(var.set_reactive_power_offset(i, conf[CONF_OFFSET_REACTIVE_POWER])) if voltage_config := conf.get(CONF_VOLTAGE): sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(i, sens)) @@ -218,16 +236,15 @@ async def to_code(config): if peak_current_config := conf.get(CONF_PEAK_CURRENT): sens = await sensor.new_sensor(peak_current_config) cg.add(var.set_peak_current_sensor(i, sens)) - if frequency_config := config.get(CONF_FREQUENCY): sens = await sensor.new_sensor(frequency_config) cg.add(var.set_freq_sensor(sens)) if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE): sens = await sensor.new_sensor(chip_temperature_config) cg.add(var.set_chip_temperature_sensor(sens)) - cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) + cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION])) diff --git a/esphome/components/atm90e32/text_sensor/__init__.py b/esphome/components/atm90e32/text_sensor/__init__.py new file mode 100644 index 0000000000..ab96f6c207 --- /dev/null +++ b/esphome/components/atm90e32/text_sensor/__init__.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C + +from ..sensor import ATM90E32Component + +CONF_PHASE_STATUS = "phase_status" +CONF_FREQUENCY_STATUS = "frequency_status" +PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C] + +PHASE_STATUS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PHASE_A): text_sensor.text_sensor_schema( + icon="mdi:flash-alert" + ), + cv.Optional(CONF_PHASE_B): text_sensor.text_sensor_schema( + icon="mdi:flash-alert" + ), + cv.Optional(CONF_PHASE_C): text_sensor.text_sensor_schema( + icon="mdi:flash-alert" + ), + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(ATM90E32Component), + cv.Optional(CONF_PHASE_STATUS): PHASE_STATUS_SCHEMA, + cv.Optional(CONF_FREQUENCY_STATUS): text_sensor.text_sensor_schema( + icon="mdi:lightbulb-alert" + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if phase_cfg := config.get(CONF_PHASE_STATUS): + for i, key in enumerate(PHASE_KEYS): + if sub_phase_cfg := phase_cfg.get(key): + sens = await text_sensor.new_text_sensor(sub_phase_cfg) + cg.add(parent.set_phase_status_text_sensor(i, sens)) + + if freq_status_config := config.get(CONF_FREQUENCY_STATUS): + sens = await text_sensor.new_text_sensor(freq_status_config) + cg.add(parent.set_freq_status_text_sensor(sens)) diff --git a/tests/components/atm90e32/common.yaml b/tests/components/atm90e32/common.yaml index 156d00b4e0..3eeed8395f 100644 --- a/tests/components/atm90e32/common.yaml +++ b/tests/components/atm90e32/common.yaml @@ -17,10 +17,22 @@ sensor: name: EMON Active Power CT1 reactive_power: name: EMON Reactive Power CT1 + apparent_power: + name: EMON Apparent Power CT1 + harmonic_power: + name: EMON Harmonic Power CT1 power_factor: name: EMON Power Factor CT1 + phase_angle: + name: EMON Phase Angle CT1 + peak_current: + name: EMON Peak Current CT1 gain_voltage: 7305 gain_ct: 27961 + offset_voltage: 0 + offset_current: 0 + offset_active_power: 0 + offset_reactive_power: 0 phase_b: current: name: EMON CT2 Current @@ -28,10 +40,22 @@ sensor: name: EMON Active Power CT2 reactive_power: name: EMON Reactive Power CT2 + apparent_power: + name: EMON Apparent Power CT2 + harmonic_power: + name: EMON Harmonic Power CT2 power_factor: name: EMON Power Factor CT2 + phase_angle: + name: EMON Phase Angle CT2 + peak_current: + name: EMON Peak Current CT2 gain_voltage: 7305 gain_ct: 27961 + offset_voltage: 0 + offset_current: 0 + offset_active_power: 0 + offset_reactive_power: 0 phase_c: current: name: EMON CT3 Current @@ -39,23 +63,75 @@ sensor: name: EMON Active Power CT3 reactive_power: name: EMON Reactive Power CT3 + apparent_power: + name: EMON Apparent Power CT3 + harmonic_power: + name: EMON Harmonic Power CT3 power_factor: name: EMON Power Factor CT3 + phase_angle: + name: EMON Phase Angle CT3 + peak_current: + name: EMON Peak Current CT3 gain_voltage: 7305 gain_ct: 27961 + offset_voltage: 0 + offset_current: 0 + offset_active_power: 0 + offset_reactive_power: 0 frequency: name: EMON Line Frequency chip_temperature: - name: EMON Chip Temp A + name: EMON Chip Temp line_frequency: 60Hz current_phases: 3 - gain_pga: 2X + gain_pga: 1X enable_offset_calibration: True + enable_gain_calibration: True + +text_sensor: + - platform: atm90e32 + id: atm90e32_chip1 + phase_status: + phase_a: + name: "Phase A Status" + phase_b: + name: "Phase B Status" + phase_c: + name: "Phase C Status" + frequency_status: + name: "Frequency Status" button: - platform: atm90e32 id: atm90e32_chip1 + run_gain_calibration: + name: "Run Gain Calibration" + clear_gain_calibration: + name: "Clear Gain Calibration" run_offset_calibration: - name: Chip1 - Run Offset Calibration + name: "Run Offset Calibration" clear_offset_calibration: - name: Chip1 - Clear Offset Calibration + name: "Clear Offset Calibration" + run_power_offset_calibration: + name: "Run Power Offset Calibration" + clear_power_offset_calibration: + name: "Clear Power Offset Calibration" + +number: + - platform: atm90e32 + id: atm90e32_chip1 + reference_voltage: + phase_a: + name: "Phase A Ref Voltage" + phase_b: + name: "Phase B Ref Voltage" + phase_c: + name: "Phase C Ref Voltage" + reference_current: + phase_a: + name: "Phase A Ref Current" + phase_b: + name: "Phase B Ref Current" + phase_c: + name: "Phase C Ref Current" From 1da8e99d27ee73900f55caea43feda61a9e07cb3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 13:33:28 +1200 Subject: [PATCH 023/193] [api] Synchronise api.proto between repos (#8720) --- esphome/components/api/api.proto | 80 ++++-- esphome/components/api/api_pb2.cpp | 301 ++++++++++++++++++++ esphome/components/api/api_pb2.h | 59 ++++ esphome/components/api/api_pb2_service.cpp | 314 ++++++++++++--------- esphome/components/api/api_pb2_service.h | 151 +++++----- 5 files changed, 684 insertions(+), 221 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 55dc3984b0..1fdf4e1339 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -33,23 +33,24 @@ service APIConnection { rpc execute_service (ExecuteServiceRequest) returns (void) {} rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} - rpc cover_command (CoverCommandRequest) returns (void) {} - rpc fan_command (FanCommandRequest) returns (void) {} - rpc light_command (LightCommandRequest) returns (void) {} - rpc switch_command (SwitchCommandRequest) returns (void) {} + rpc button_command (ButtonCommandRequest) returns (void) {} rpc camera_image (CameraImageRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {} - rpc number_command (NumberCommandRequest) returns (void) {} - rpc text_command (TextCommandRequest) returns (void) {} - rpc select_command (SelectCommandRequest) returns (void) {} - rpc button_command (ButtonCommandRequest) returns (void) {} - rpc lock_command (LockCommandRequest) returns (void) {} - rpc valve_command (ValveCommandRequest) returns (void) {} - rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} + rpc cover_command (CoverCommandRequest) returns (void) {} rpc date_command (DateCommandRequest) returns (void) {} - rpc time_command (TimeCommandRequest) returns (void) {} rpc datetime_command (DateTimeCommandRequest) returns (void) {} + rpc fan_command (FanCommandRequest) returns (void) {} + rpc light_command (LightCommandRequest) returns (void) {} + rpc lock_command (LockCommandRequest) returns (void) {} + rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} + rpc number_command (NumberCommandRequest) returns (void) {} + rpc select_command (SelectCommandRequest) returns (void) {} + rpc siren_command (SirenCommandRequest) returns (void) {} + rpc switch_command (SwitchCommandRequest) returns (void) {} + rpc text_command (TextCommandRequest) returns (void) {} + rpc time_command (TimeCommandRequest) returns (void) {} rpc update_command (UpdateCommandRequest) returns (void) {} + rpc valve_command (ValveCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} @@ -655,7 +656,7 @@ message SubscribeLogsResponse { option (no_delay) = false; LogLevel level = 1; - string message = 3; + bytes message = 3; bool send_failed = 4; } @@ -911,6 +912,7 @@ message ClimateStateResponse { float target_temperature = 4; float target_temperature_low = 5; float target_temperature_high = 6; + // For older peers, equal to preset == CLIMATE_PRESET_AWAY bool unused_legacy_away = 7; ClimateAction action = 8; ClimateFanMode fan_mode = 9; @@ -936,6 +938,7 @@ message ClimateCommandRequest { float target_temperature_low = 7; bool has_target_temperature_high = 8; float target_temperature_high = 9; + // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset bool unused_has_legacy_away = 10; bool unused_legacy_away = 11; bool has_fan_mode = 12; @@ -1038,6 +1041,49 @@ message SelectCommandRequest { string state = 2; } +// ==================== SIREN ==================== +message ListEntitiesSirenResponse { + option (id) = 55; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SIREN"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + repeated string tones = 7; + bool supports_duration = 8; + bool supports_volume = 9; + EntityCategory entity_category = 10; +} +message SirenStateResponse { + option (id) = 56; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SIREN"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; +} +message SirenCommandRequest { + option (id) = 57; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SIREN"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_state = 2; + bool state = 3; + bool has_tone = 4; + string tone = 5; + bool has_duration = 6; + uint32 duration = 7; + bool has_volume = 8; + float volume = 9; +} // ==================== LOCK ==================== enum LockState { @@ -1207,8 +1253,8 @@ message SubscribeBluetoothLEAdvertisementsRequest { message BluetoothServiceData { string uuid = 1; - repeated uint32 legacy_data = 2 [deprecated = true]; - bytes data = 3; // Changed in proto version 1.7 + repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7 + bytes data = 3; // Added in api version 1.7 } message BluetoothLEAdvertisementResponse { option (id) = 67; @@ -1217,7 +1263,7 @@ message BluetoothLEAdvertisementResponse { option (no_delay) = true; uint64 address = 1; - string name = 2; + bytes name = 2; sint32 rssi = 3; repeated string service_uuids = 4; @@ -1504,7 +1550,7 @@ message BluetoothScannerSetModeRequest { BluetoothScannerMode mode = 1; } -// ==================== PUSH TO TALK ==================== +// ==================== VOICE ASSISTANT ==================== enum VoiceAssistantSubscribeFlag { VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index b5d8bb3d79..e3181b6166 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -5377,6 +5377,307 @@ void SelectCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 8: { + this->supports_duration = value.as_bool(); + return true; + } + case 9: { + this->supports_volume = value.as_bool(); + return true; + } + case 10: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSirenResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 7: { + this->tones.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesSirenResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + for (auto &it : this->tones) { + buffer.encode_string(7, it, true); + } + buffer.encode_bool(8, this->supports_duration); + buffer.encode_bool(9, this->supports_volume); + buffer.encode_enum(10, this->entity_category); +} +void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_string_field(total_size, 1, this->object_id, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + if (!this->tones.empty()) { + for (const auto &it : this->tones) { + ProtoSize::add_string_field(total_size, 1, it, true); + } + } + ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false); + ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesSirenResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesSirenResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + for (const auto &it : this->tones) { + out.append(" tones: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" supports_duration: "); + out.append(YESNO(this->supports_duration)); + out.append("\n"); + + out.append(" supports_volume: "); + out.append(YESNO(this->supports_volume)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + out.append("}"); +} +#endif +bool SirenStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SirenStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->state); +} +void SirenStateResponse::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SirenStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SirenStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + out.append("}"); +} +#endif +bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_state = value.as_bool(); + return true; + } + case 3: { + this->state = value.as_bool(); + return true; + } + case 4: { + this->has_tone = value.as_bool(); + return true; + } + case 6: { + this->has_duration = value.as_bool(); + return true; + } + case 7: { + this->duration = value.as_uint32(); + return true; + } + case 8: { + this->has_volume = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 5: { + this->tone = value.as_string(); + return true; + } + default: + return false; + } +} +bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 9: { + this->volume = value.as_float(); + return true; + } + default: + return false; + } +} +void SirenCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_state); + buffer.encode_bool(3, this->state); + buffer.encode_bool(4, this->has_tone); + buffer.encode_string(5, this->tone); + buffer.encode_bool(6, this->has_duration); + buffer.encode_uint32(7, this->duration); + buffer.encode_bool(8, this->has_volume); + buffer.encode_float(9, this->volume); +} +void SirenCommandRequest::calculate_size(uint32_t &total_size) const { + ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_bool_field(total_size, 1, this->has_state, false); + ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_bool_field(total_size, 1, this->has_tone, false); + ProtoSize::add_string_field(total_size, 1, this->tone, false); + ProtoSize::add_bool_field(total_size, 1, this->has_duration, false); + ProtoSize::add_uint32_field(total_size, 1, this->duration, false); + ProtoSize::add_bool_field(total_size, 1, this->has_volume, false); + ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SirenCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SirenCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_state: "); + out.append(YESNO(this->has_state)); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" has_tone: "); + out.append(YESNO(this->has_tone)); + out.append("\n"); + + out.append(" tone: "); + out.append("'").append(this->tone).append("'"); + out.append("\n"); + + out.append(" has_duration: "); + out.append(YESNO(this->has_duration)); + out.append("\n"); + + out.append(" duration: "); + sprintf(buffer, "%" PRIu32, this->duration); + out.append(buffer); + out.append("\n"); + + out.append(" has_volume: "); + out.append(YESNO(this->has_volume)); + out.append("\n"); + + out.append(" volume: "); + sprintf(buffer, "%g", this->volume); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 457797f1a7..c0927ebdc0 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1284,6 +1284,65 @@ class SelectCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +class ListEntitiesSirenResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + std::vector tones{}; + bool supports_duration{false}; + bool supports_volume{false}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SirenStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool state{false}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SirenCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + bool has_state{false}; + bool state{false}; + bool has_tone{false}; + std::string tone{}; + bool has_duration{false}; + uint32_t duration{0}; + bool has_volume{false}; + float volume{0.0f}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class ListEntitiesLockResponse : public ProtoMessage { public: std::string object_id{}; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index cde4492000..5a701aeafa 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -292,6 +292,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon #endif #ifdef USE_SELECT #endif +#ifdef USE_SIREN +bool APIServerConnectionBase::send_list_entities_siren_response(const ListEntitiesSirenResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_siren_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 55); +} +#endif +#ifdef USE_SIREN +bool APIServerConnectionBase::send_siren_state_response(const SirenStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_siren_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 56); +} +#endif +#ifdef USE_SIREN +#endif #ifdef USE_LOCK bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP @@ -903,6 +921,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); #endif this->on_select_command_request(msg); +#endif + break; + } + case 57: { +#ifdef USE_SIREN + SirenCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str()); +#endif + this->on_siren_command_request(msg); #endif break; } @@ -1369,8 +1398,8 @@ void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncrypt } } #endif -#ifdef USE_COVER -void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { +#ifdef USE_BUTTON +void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { if (!this->is_connection_setup()) { this->on_no_setup_connection(); return; @@ -1379,46 +1408,7 @@ void APIServerConnection::on_cover_command_request(const CoverCommandRequest &ms this->on_unauthenticated_access(); return; } - this->cover_command(msg); -} -#endif -#ifdef USE_FAN -void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->fan_command(msg); -} -#endif -#ifdef USE_LIGHT -void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->light_command(msg); -} -#endif -#ifdef USE_SWITCH -void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->switch_command(msg); + this->button_command(msg); } #endif #ifdef USE_ESP32_CAMERA @@ -1447,8 +1437,8 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest this->climate_command(msg); } #endif -#ifdef USE_NUMBER -void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { +#ifdef USE_COVER +void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { if (!this->is_connection_setup()) { this->on_no_setup_connection(); return; @@ -1457,85 +1447,7 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest & this->on_unauthenticated_access(); return; } - this->number_command(msg); -} -#endif -#ifdef USE_TEXT -void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->text_command(msg); -} -#endif -#ifdef USE_SELECT -void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->select_command(msg); -} -#endif -#ifdef USE_BUTTON -void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->button_command(msg); -} -#endif -#ifdef USE_LOCK -void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->lock_command(msg); -} -#endif -#ifdef USE_VALVE -void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->valve_command(msg); -} -#endif -#ifdef USE_MEDIA_PLAYER -void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->media_player_command(msg); + this->cover_command(msg); } #endif #ifdef USE_DATETIME_DATE @@ -1551,19 +1463,6 @@ void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) this->date_command(msg); } #endif -#ifdef USE_DATETIME_TIME -void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->time_command(msg); -} -#endif #ifdef USE_DATETIME_DATETIME void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { if (!this->is_connection_setup()) { @@ -1577,6 +1476,136 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ this->datetime_command(msg); } #endif +#ifdef USE_FAN +void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->fan_command(msg); +} +#endif +#ifdef USE_LIGHT +void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->light_command(msg); +} +#endif +#ifdef USE_LOCK +void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->lock_command(msg); +} +#endif +#ifdef USE_MEDIA_PLAYER +void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->media_player_command(msg); +} +#endif +#ifdef USE_NUMBER +void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->number_command(msg); +} +#endif +#ifdef USE_SELECT +void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->select_command(msg); +} +#endif +#ifdef USE_SIREN +void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->siren_command(msg); +} +#endif +#ifdef USE_SWITCH +void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->switch_command(msg); +} +#endif +#ifdef USE_TEXT +void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->text_command(msg); +} +#endif +#ifdef USE_DATETIME_TIME +void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->time_command(msg); +} +#endif #ifdef USE_UPDATE void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { if (!this->is_connection_setup()) { @@ -1590,6 +1619,19 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest & this->update_command(msg); } #endif +#ifdef USE_VALVE +void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->valve_command(msg); +} +#endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 4964f41420..8ee5c0fcf1 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -136,6 +136,15 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_SELECT virtual void on_select_command_request(const SelectCommandRequest &value){}; #endif +#ifdef USE_SIREN + bool send_list_entities_siren_response(const ListEntitiesSirenResponse &msg); +#endif +#ifdef USE_SIREN + bool send_siren_state_response(const SirenStateResponse &msg); +#endif +#ifdef USE_SIREN + virtual void on_siren_command_request(const SirenCommandRequest &value){}; +#endif #ifdef USE_LOCK bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg); #endif @@ -364,17 +373,8 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_NOISE virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; #endif -#ifdef USE_COVER - virtual void cover_command(const CoverCommandRequest &msg) = 0; -#endif -#ifdef USE_FAN - virtual void fan_command(const FanCommandRequest &msg) = 0; -#endif -#ifdef USE_LIGHT - virtual void light_command(const LightCommandRequest &msg) = 0; -#endif -#ifdef USE_SWITCH - virtual void switch_command(const SwitchCommandRequest &msg) = 0; +#ifdef USE_BUTTON + virtual void button_command(const ButtonCommandRequest &msg) = 0; #endif #ifdef USE_ESP32_CAMERA virtual void camera_image(const CameraImageRequest &msg) = 0; @@ -382,39 +382,51 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_CLIMATE virtual void climate_command(const ClimateCommandRequest &msg) = 0; #endif -#ifdef USE_NUMBER - virtual void number_command(const NumberCommandRequest &msg) = 0; -#endif -#ifdef USE_TEXT - virtual void text_command(const TextCommandRequest &msg) = 0; -#endif -#ifdef USE_SELECT - virtual void select_command(const SelectCommandRequest &msg) = 0; -#endif -#ifdef USE_BUTTON - virtual void button_command(const ButtonCommandRequest &msg) = 0; -#endif -#ifdef USE_LOCK - virtual void lock_command(const LockCommandRequest &msg) = 0; -#endif -#ifdef USE_VALVE - virtual void valve_command(const ValveCommandRequest &msg) = 0; -#endif -#ifdef USE_MEDIA_PLAYER - virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; +#ifdef USE_COVER + virtual void cover_command(const CoverCommandRequest &msg) = 0; #endif #ifdef USE_DATETIME_DATE virtual void date_command(const DateCommandRequest &msg) = 0; #endif -#ifdef USE_DATETIME_TIME - virtual void time_command(const TimeCommandRequest &msg) = 0; -#endif #ifdef USE_DATETIME_DATETIME virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; #endif +#ifdef USE_FAN + virtual void fan_command(const FanCommandRequest &msg) = 0; +#endif +#ifdef USE_LIGHT + virtual void light_command(const LightCommandRequest &msg) = 0; +#endif +#ifdef USE_LOCK + virtual void lock_command(const LockCommandRequest &msg) = 0; +#endif +#ifdef USE_MEDIA_PLAYER + virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; +#endif +#ifdef USE_NUMBER + virtual void number_command(const NumberCommandRequest &msg) = 0; +#endif +#ifdef USE_SELECT + virtual void select_command(const SelectCommandRequest &msg) = 0; +#endif +#ifdef USE_SIREN + virtual void siren_command(const SirenCommandRequest &msg) = 0; +#endif +#ifdef USE_SWITCH + virtual void switch_command(const SwitchCommandRequest &msg) = 0; +#endif +#ifdef USE_TEXT + virtual void text_command(const TextCommandRequest &msg) = 0; +#endif +#ifdef USE_DATETIME_TIME + virtual void time_command(const TimeCommandRequest &msg) = 0; +#endif #ifdef USE_UPDATE virtual void update_command(const UpdateCommandRequest &msg) = 0; #endif +#ifdef USE_VALVE + virtual void valve_command(const ValveCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -478,17 +490,8 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_NOISE void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; #endif -#ifdef USE_COVER - void on_cover_command_request(const CoverCommandRequest &msg) override; -#endif -#ifdef USE_FAN - void on_fan_command_request(const FanCommandRequest &msg) override; -#endif -#ifdef USE_LIGHT - void on_light_command_request(const LightCommandRequest &msg) override; -#endif -#ifdef USE_SWITCH - void on_switch_command_request(const SwitchCommandRequest &msg) override; +#ifdef USE_BUTTON + void on_button_command_request(const ButtonCommandRequest &msg) override; #endif #ifdef USE_ESP32_CAMERA void on_camera_image_request(const CameraImageRequest &msg) override; @@ -496,39 +499,51 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_CLIMATE void on_climate_command_request(const ClimateCommandRequest &msg) override; #endif -#ifdef USE_NUMBER - void on_number_command_request(const NumberCommandRequest &msg) override; -#endif -#ifdef USE_TEXT - void on_text_command_request(const TextCommandRequest &msg) override; -#endif -#ifdef USE_SELECT - void on_select_command_request(const SelectCommandRequest &msg) override; -#endif -#ifdef USE_BUTTON - void on_button_command_request(const ButtonCommandRequest &msg) override; -#endif -#ifdef USE_LOCK - void on_lock_command_request(const LockCommandRequest &msg) override; -#endif -#ifdef USE_VALVE - void on_valve_command_request(const ValveCommandRequest &msg) override; -#endif -#ifdef USE_MEDIA_PLAYER - void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; +#ifdef USE_COVER + void on_cover_command_request(const CoverCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATE void on_date_command_request(const DateCommandRequest &msg) override; #endif -#ifdef USE_DATETIME_TIME - void on_time_command_request(const TimeCommandRequest &msg) override; -#endif #ifdef USE_DATETIME_DATETIME void on_date_time_command_request(const DateTimeCommandRequest &msg) override; #endif +#ifdef USE_FAN + void on_fan_command_request(const FanCommandRequest &msg) override; +#endif +#ifdef USE_LIGHT + void on_light_command_request(const LightCommandRequest &msg) override; +#endif +#ifdef USE_LOCK + void on_lock_command_request(const LockCommandRequest &msg) override; +#endif +#ifdef USE_MEDIA_PLAYER + void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; +#endif +#ifdef USE_NUMBER + void on_number_command_request(const NumberCommandRequest &msg) override; +#endif +#ifdef USE_SELECT + void on_select_command_request(const SelectCommandRequest &msg) override; +#endif +#ifdef USE_SIREN + void on_siren_command_request(const SirenCommandRequest &msg) override; +#endif +#ifdef USE_SWITCH + void on_switch_command_request(const SwitchCommandRequest &msg) override; +#endif +#ifdef USE_TEXT + void on_text_command_request(const TextCommandRequest &msg) override; +#endif +#ifdef USE_DATETIME_TIME + void on_time_command_request(const TimeCommandRequest &msg) override; +#endif #ifdef USE_UPDATE void on_update_command_request(const UpdateCommandRequest &msg) override; #endif +#ifdef USE_VALVE + void on_valve_command_request(const ValveCommandRequest &msg) override; +#endif #ifdef USE_BLUETOOTH_PROXY void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; #endif From 2b3757dff85f8a91583cee25cf43b496927f430e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 18:05:26 +1200 Subject: [PATCH 024/193] [valve] Tidy up template publish action location (#8731) --- esphome/components/template/valve/__init__.py | 12 ++++++---- .../components/template/valve/automation.h | 24 +++++++++++++++++++ esphome/components/valve/automation.h | 20 +--------------- tests/components/template/common.yaml | 2 ++ 4 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 esphome/components/template/valve/automation.h diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py index 12e5174168..c3f529ebd4 100644 --- a/esphome/components/template/valve/__init__.py +++ b/esphome/components/template/valve/__init__.py @@ -21,6 +21,10 @@ from .. import template_ns TemplateValve = template_ns.class_("TemplateValve", valve.Valve, cg.Component) +TemplateValvePublishAction = template_ns.class_( + "TemplateValvePublishAction", automation.Action, cg.Parented.template(TemplateValve) +) + TemplateValveRestoreMode = template_ns.enum("TemplateValveRestoreMode") RESTORE_MODES = { "NO_RESTORE": TemplateValveRestoreMode.VALVE_NO_RESTORE, @@ -90,10 +94,10 @@ async def to_code(config): @automation.register_action( "valve.template.publish", - valve.ValvePublishAction, + TemplateValvePublishAction, cv.Schema( { - cv.Required(CONF_ID): cv.use_id(valve.Valve), + cv.GenerateID(): cv.use_id(TemplateValve), cv.Exclusive(CONF_STATE, "pos"): cv.templatable(valve.validate_valve_state), cv.Exclusive(CONF_POSITION, "pos"): cv.templatable(cv.percentage), cv.Optional(CONF_CURRENT_OPERATION): cv.templatable( @@ -103,8 +107,8 @@ async def to_code(config): ), ) async def valve_template_publish_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) if state_config := config.get(CONF_STATE): template_ = await cg.templatable(state_config, args, float) cg.add(var.set_position(template_)) diff --git a/esphome/components/template/valve/automation.h b/esphome/components/template/valve/automation.h new file mode 100644 index 0000000000..af9b070c60 --- /dev/null +++ b/esphome/components/template/valve/automation.h @@ -0,0 +1,24 @@ +#pragma once + +#include "template_valve.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace template_ { + +template class TemplateValvePublishAction : public Action, public Parented { + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(valve::ValveOperation, current_operation) + + void play(Ts... x) override { + if (this->position_.has_value()) + this->parent_->position = this->position_.value(x...); + if (this->current_operation_.has_value()) + this->parent_->current_operation = this->current_operation_.value(x...); + this->parent_->publish_state(); + } +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 24c94a5570..f2c06270c0 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "valve.h" namespace esphome { @@ -67,24 +67,6 @@ template class ControlAction : public Action { Valve *valve_; }; -template class ValvePublishAction : public Action { - public: - ValvePublishAction(Valve *valve) : valve_(valve) {} - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(ValveOperation, current_operation) - - void play(Ts... x) override { - if (this->position_.has_value()) - this->valve_->position = this->position_.value(x...); - if (this->current_operation_.has_value()) - this->valve_->current_operation = this->current_operation_.value(x...); - this->valve_->publish_state(); - } - - protected: - Valve *valve_; -}; - template class ValveIsOpenCondition : public Condition { public: ValveIsOpenCondition(Valve *valve) : valve_(valve) {} diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml index 987849a80c..fd9342b3e5 100644 --- a/tests/components/template/common.yaml +++ b/tests/components/template/common.yaml @@ -174,6 +174,8 @@ valve: - logger.log: open_action close_action: - logger.log: close_action + - valve.template.publish: + state: CLOSED stop_action: - logger.log: stop_action optimistic: true From 23fb1bed6125fdccd10eb577d474cce690fa8873 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 18:14:13 +1200 Subject: [PATCH 025/193] [valve] Move to use ``valve_schema(..)`` instead of ``VALVE_SCHEMA`` (#8730) --- esphome/components/template/valve/__init__.py | 41 +++++++++++-------- esphome/components/valve/__init__.py | 32 +++++++++++++-- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py index c3f529ebd4..526751564d 100644 --- a/esphome/components/template/valve/__init__.py +++ b/esphome/components/template/valve/__init__.py @@ -35,23 +35,30 @@ RESTORE_MODES = { CONF_HAS_POSITION = "has_position" CONF_TOGGLE_ACTION = "toggle_action" -CONFIG_SCHEMA = valve.VALVE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateValve), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, - cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, - cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( - RESTORE_MODES, upper=True - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + valve.valve_schema(TemplateValve) + .extend( + { + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, + cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index e55bb522de..76ad76e8d0 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_MQTT_ID, CONF_ON_OPEN, @@ -20,6 +22,7 @@ from esphome.const import ( DEVICE_CLASS_WATER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -71,7 +74,7 @@ ValveClosedTrigger = valve_ns.class_( CONF_ON_CLOSED = "on_closed" -VALVE_SCHEMA = ( +_VALVE_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( @@ -100,7 +103,30 @@ VALVE_SCHEMA = ( ) -async def setup_valve_core_(var, config): +def valve_schema( + class_: MockObjClass = cv.UNDEFINED, + *, + device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {} + + if class_ is not cv.UNDEFINED: + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _VALVE_SCHEMA.extend(schema) + + +async def _setup_valve_core(var, config): await setup_entity(var, config) if device_class_config := config.get(CONF_DEVICE_CLASS): @@ -132,7 +158,7 @@ async def register_valve(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_valve(var)) - await setup_valve_core_(var, config) + await _setup_valve_core(var, config) async def new_valve(config, *args): From b3400a1308e90c4e588b130ed983a534ec308500 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 18:19:03 +1200 Subject: [PATCH 026/193] [lock] Tidy up template publish action and lockstate locations (#8729) --- esphome/components/lock/__init__.py | 14 +++++++- esphome/components/lock/automation.h | 15 ++------ esphome/components/template/lock/__init__.py | 35 ++++++++----------- esphome/components/template/lock/automation.h | 18 ++++++++++ tests/components/lock/common.yaml | 4 +-- 5 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 esphome/components/template/lock/automation.h diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 6925861b52..764bd91f76 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -31,6 +31,18 @@ LockCondition = lock_ns.class_("LockCondition", Condition) LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) +LockState = lock_ns.enum("LockState") + +LOCK_STATES = { + "LOCKED": LockState.LOCK_STATE_LOCKED, + "UNLOCKED": LockState.LOCK_STATE_UNLOCKED, + "JAMMED": LockState.LOCK_STATE_JAMMED, + "LOCKING": LockState.LOCK_STATE_LOCKING, + "UNLOCKING": LockState.LOCK_STATE_UNLOCKING, +} + +validate_lock_state = cv.enum(LOCK_STATES, upper=True) + LOCK_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) @@ -79,7 +91,7 @@ async def register_lock(var, config): LOCK_ACTION_SCHEMA = maybe_simple_id( { - cv.Required(CONF_ID): cv.use_id(Lock), + cv.GenerateID(CONF_ID): cv.use_id(Lock), } ) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 74cfbe2ef6..8cb3b64ffe 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/automation.h" #include "esphome/components/lock/lock.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" namespace esphome { namespace lock { @@ -72,16 +72,5 @@ class LockUnlockTrigger : public Trigger<> { } }; -template class LockPublishAction : public Action { - public: - LockPublishAction(Lock *a_lock) : lock_(a_lock) {} - TEMPLATABLE_VALUE(LockState, state) - - void play(Ts... x) override { this->lock_->publish_state(this->state_.value(x...)); } - - protected: - Lock *lock_; -}; - } // namespace lock } // namespace esphome diff --git a/esphome/components/template/lock/__init__.py b/esphome/components/template/lock/__init__.py index 2dcb90e038..43a633aedf 100644 --- a/esphome/components/template/lock/__init__.py +++ b/esphome/components/template/lock/__init__.py @@ -17,17 +17,11 @@ from .. import template_ns TemplateLock = template_ns.class_("TemplateLock", lock.Lock, cg.Component) -LockState = lock.lock_ns.enum("LockState") - -LOCK_STATES = { - "LOCKED": LockState.LOCK_STATE_LOCKED, - "UNLOCKED": LockState.LOCK_STATE_UNLOCKED, - "JAMMED": LockState.LOCK_STATE_JAMMED, - "LOCKING": LockState.LOCK_STATE_LOCKING, - "UNLOCKING": LockState.LOCK_STATE_UNLOCKING, -} - -validate_lock_state = cv.enum(LOCK_STATES, upper=True) +TemplateLockPublishAction = template_ns.class_( + "TemplateLockPublishAction", + automation.Action, + cg.Parented.template(TemplateLock), +) def validate(config): @@ -66,7 +60,7 @@ async def to_code(config): if CONF_LAMBDA in config: template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.optional.template(LockState) + config[CONF_LAMBDA], [], return_type=cg.optional.template(lock.LockState) ) cg.add(var.set_state_lambda(template_)) if CONF_UNLOCK_ACTION in config: @@ -88,17 +82,18 @@ async def to_code(config): @automation.register_action( "lock.template.publish", - lock.LockPublishAction, - cv.Schema( + TemplateLockPublishAction, + cv.maybe_simple_value( { - cv.Required(CONF_ID): cv.use_id(lock.Lock), - cv.Required(CONF_STATE): cv.templatable(validate_lock_state), - } + cv.GenerateID(): cv.use_id(TemplateLock), + cv.Required(CONF_STATE): cv.templatable(lock.validate_lock_state), + }, + key=CONF_STATE, ), ) async def lock_template_publish_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, LockState) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_STATE], args, lock.LockState) cg.add(var.set_state(template_)) return var diff --git a/esphome/components/template/lock/automation.h b/esphome/components/template/lock/automation.h new file mode 100644 index 0000000000..6124546592 --- /dev/null +++ b/esphome/components/template/lock/automation.h @@ -0,0 +1,18 @@ +#pragma once + +#include "template_lock.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace template_ { + +template class TemplateLockPublishAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(lock::LockState, state) + + void play(Ts... x) override { this->parent_->publish_state(this->state_.value(x...)); } +}; + +} // namespace template_ +} // namespace esphome diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml index 82297a3da4..67da46653c 100644 --- a/tests/components/lock/common.yaml +++ b/tests/components/lock/common.yaml @@ -27,9 +27,7 @@ lock: id: test_lock1 state: !lambda "return LOCK_STATE_UNLOCKED;" on_lock: - - lock.template.publish: - id: test_lock1 - state: !lambda "return LOCK_STATE_LOCKED;" + - lock.template.publish: LOCKED - platform: output name: Generic Output Lock id: test_lock2 From 8a90ce882a373d9f682231dd23b1bb31438de340 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 18:22:43 +1200 Subject: [PATCH 027/193] [update] Move to use ``update_schema(..)`` instead of ``UPDATE_SCHEMA`` (#8726) --- .../http_request/update/__init__.py | 19 +++++++------ esphome/components/update/__init__.py | 27 ++++++++++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py index f1b282d891..abb4b2a430 100644 --- a/esphome/components/http_request/update/__init__.py +++ b/esphome/components/http_request/update/__init__.py @@ -16,14 +16,17 @@ HttpRequestUpdate = http_request_ns.class_( CONF_OTA_ID = "ota_id" -CONFIG_SCHEMA = update.UPDATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HttpRequestUpdate), - cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), - cv.Required(CONF_SOURCE): cv.url, - } -).extend(cv.polling_component_schema("6h")) +CONFIG_SCHEMA = ( + update.update_schema(HttpRequestUpdate) + .extend( + { + cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + cv.Required(CONF_SOURCE): cv.url, + } + ) + .extend(cv.polling_component_schema("6h")) +) async def to_code(config): diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 4729d954ee..a607aefea0 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_FORCE_UPDATE, + CONF_ICON, CONF_ID, CONF_MQTT_ID, CONF_WEB_SERVER, @@ -14,6 +15,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -38,7 +40,7 @@ DEVICE_CLASSES = [ CONF_ON_UPDATE_AVAILABLE = "on_update_available" -UPDATE_SCHEMA = ( +_UPDATE_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( @@ -56,6 +58,29 @@ UPDATE_SCHEMA = ( ) +def update_schema( + class_: MockObjClass = cv.UNDEFINED, + *, + icon: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {} + + if class_ is not cv.UNDEFINED: + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _UPDATE_SCHEMA.extend(schema) + + async def setup_update_core_(var, config): await setup_entity(var, config) From ca221d6cb2d51deb22329b6a4271623cc21d7b85 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 18:24:34 +1200 Subject: [PATCH 028/193] [text] Move to use ``text_schema(..)`` instead of ``TEXT_SCHEMA`` (#8727) --- esphome/components/copy/text/__init__.py | 15 ++++++----- esphome/components/lvgl/text/__init__.py | 3 +-- esphome/components/template/text/__init__.py | 7 ++--- esphome/components/text/__init__.py | 28 +++++++++++++++++++- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/esphome/components/copy/text/__init__.py b/esphome/components/copy/text/__init__.py index aa39225bc2..f1ca404b7b 100644 --- a/esphome/components/copy/text/__init__.py +++ b/esphome/components/copy/text/__init__.py @@ -9,12 +9,15 @@ from .. import copy_ns CopyText = copy_ns.class_("CopyText", text.Text, cg.Component) -CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyText), - cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + text.text_schema(CopyText) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index 89db139a6a..eb56cdb7a7 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -19,9 +19,8 @@ from ..widgets import get_widgets, wait_for_widgets LVGLText = lvgl_ns.class_("LVGLText", text.Text) -CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( +CONFIG_SCHEMA = text.text_schema(LVGLText).extend( { - cv.GenerateID(): cv.declare_id(LVGLText), cv.Required(CONF_WIDGET): cv.use_id(LvText), } ) diff --git a/esphome/components/template/text/__init__.py b/esphome/components/template/text/__init__.py index b0fea38aaf..572b5ba0f4 100644 --- a/esphome/components/template/text/__init__.py +++ b/esphome/components/template/text/__init__.py @@ -46,9 +46,9 @@ def validate(config): CONFIG_SCHEMA = cv.All( - text.TEXT_SCHEMA.extend( + text.text_schema(TemplateText) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateText), cv.Optional(CONF_MIN_LENGTH, default=0): cv.int_range(min=0, max=255), cv.Optional(CONF_MAX_LENGTH, default=255): cv.int_range(min=0, max=255), cv.Optional(CONF_PATTERN): cv.string, @@ -58,7 +58,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate, ) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 20e5a645d1..39626c2c5c 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -5,6 +5,8 @@ import esphome.codegen as cg from esphome.components import mqtt, web_server import esphome.config_validation as cv from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_MODE, CONF_MQTT_ID, @@ -14,6 +16,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@mauritskorse"] @@ -39,7 +42,7 @@ TEXT_MODES = { "PASSWORD": TextMode.TEXT_MODE_PASSWORD, # to be implemented for keys, passwords, etc. } -TEXT_SCHEMA = ( +_TEXT_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) .extend( @@ -57,6 +60,29 @@ TEXT_SCHEMA = ( ) +def text_schema( + class_: MockObjClass = cv.UNDEFINED, + *, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + mode: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {} + + if class_ is not cv.UNDEFINED: + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_MODE, mode, cv.enum(TEXT_MODES, upper=True)), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _TEXT_SCHEMA.extend(schema) + + async def setup_text_core_( var, config, From e1732c4945f41c002c18ee926f1c84be27dee1b6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 19:45:32 +1200 Subject: [PATCH 029/193] [lock] Move to use ``lock_schema(..)`` instead of ``LOCK_SCHEMA`` (#8728) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Keith Burzinski --- esphome/components/copy/lock/__init__.py | 20 ++++++----- esphome/components/lock/__init__.py | 36 ++++++++++++++++++-- esphome/components/output/lock/__init__.py | 20 ++++++----- esphome/components/template/lock/__init__.py | 10 +++--- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/esphome/components/copy/lock/__init__.py b/esphome/components/copy/lock/__init__.py index ddedea64c0..46bc08273e 100644 --- a/esphome/components/copy/lock/__init__.py +++ b/esphome/components/copy/lock/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import lock import esphome.config_validation as cv -from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID +from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from from .. import copy_ns @@ -9,12 +9,15 @@ from .. import copy_ns CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component) -CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyLock), - cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + lock.lock_schema(CopyLock) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), @@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await lock.register_lock(var, config) + var = await lock.new_lock(config) await cg.register_component(var, config) source = await cg.get_variable(config[CONF_SOURCE_ID]) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 764bd91f76..8bf7af3de2 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -4,6 +4,8 @@ import esphome.codegen as cg from esphome.components import mqtt, web_server import esphome.config_validation as cv from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_MQTT_ID, CONF_ON_LOCK, @@ -12,6 +14,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] @@ -43,7 +46,7 @@ LOCK_STATES = { validate_lock_state = cv.enum(LOCK_STATES, upper=True) -LOCK_SCHEMA = ( +_LOCK_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( @@ -64,7 +67,28 @@ LOCK_SCHEMA = ( ) -async def setup_lock_core_(var, config): +def lock_schema( + class_: MockObjClass = cv.UNDEFINED, + *, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {} + + if class_ is not cv.UNDEFINED: + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _LOCK_SCHEMA.extend(schema) + + +async def _setup_lock_core(var, config): await setup_entity(var, config) for conf in config.get(CONF_ON_LOCK, []): @@ -86,7 +110,13 @@ async def register_lock(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_lock(var)) - await setup_lock_core_(var, config) + await _setup_lock_core(var, config) + + +async def new_lock(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_lock(var, config) + return var LOCK_ACTION_SCHEMA = maybe_simple_id( diff --git a/esphome/components/output/lock/__init__.py b/esphome/components/output/lock/__init__.py index c9bdba0f75..553114b689 100644 --- a/esphome/components/output/lock/__init__.py +++ b/esphome/components/output/lock/__init__.py @@ -1,24 +1,26 @@ import esphome.codegen as cg from esphome.components import lock, output import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_OUTPUT +from esphome.const import CONF_OUTPUT from .. import output_ns OutputLock = output_ns.class_("OutputLock", lock.Lock, cg.Component) -CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(OutputLock), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + lock.lock_schema(OutputLock) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await lock.new_lock(config) await cg.register_component(var, config) - await lock.register_lock(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/template/lock/__init__.py b/esphome/components/template/lock/__init__.py index 43a633aedf..4c74a521fa 100644 --- a/esphome/components/template/lock/__init__.py +++ b/esphome/components/template/lock/__init__.py @@ -36,9 +36,9 @@ def validate(config): CONFIG_SCHEMA = cv.All( - lock.LOCK_SCHEMA.extend( + lock.lock_schema(TemplateLock) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateLock), cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, @@ -48,15 +48,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_LOCK_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate, ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await lock.new_lock(config) await cg.register_component(var, config) - await lock.register_lock(var, config) if CONF_LAMBDA in config: template_ = await cg.process_lambda( From 8399d894c159f5eb8fa278993fc84769a140d5ef Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 20:18:52 +1200 Subject: [PATCH 030/193] [config] Use ``cv.UNDEFINED`` instead of adhoc ``_UNDEF`` objects (#8725) --- esphome/components/audio/__init__.py | 43 +++++++++---------- esphome/components/binary_sensor/__init__.py | 14 +++--- esphome/components/button/__init__.py | 10 ++--- esphome/components/event/__init__.py | 14 +++--- esphome/components/microphone/__init__.py | 7 +-- esphome/components/number/__init__.py | 12 +++--- .../opentherm/binary_sensor/__init__.py | 4 +- .../components/opentherm/sensor/__init__.py | 6 +-- esphome/components/select/__init__.py | 14 +++--- esphome/components/sensor/__init__.py | 24 +++++------ esphome/components/switch/__init__.py | 18 ++++---- esphome/components/text_sensor/__init__.py | 18 ++++---- 12 files changed, 82 insertions(+), 102 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 9f08c81e77..06f3cf09d8 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -37,16 +37,13 @@ AUDIO_COMPONENT_SCHEMA = cv.Schema( ) -_UNDEF = object() - - def set_stream_limits( - min_bits_per_sample: int = _UNDEF, - max_bits_per_sample: int = _UNDEF, - min_channels: int = _UNDEF, - max_channels: int = _UNDEF, - min_sample_rate: int = _UNDEF, - max_sample_rate: int = _UNDEF, + min_bits_per_sample: int = cv.UNDEFINED, + max_bits_per_sample: int = cv.UNDEFINED, + min_channels: int = cv.UNDEFINED, + max_channels: int = cv.UNDEFINED, + min_sample_rate: int = cv.UNDEFINED, + max_sample_rate: int = cv.UNDEFINED, ): """Sets the limits for the audio stream that audio component can handle @@ -55,17 +52,17 @@ def set_stream_limits( """ def set_limits_in_config(config): - if min_bits_per_sample is not _UNDEF: + if min_bits_per_sample is not cv.UNDEFINED: config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample - if max_bits_per_sample is not _UNDEF: + if max_bits_per_sample is not cv.UNDEFINED: config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample - if min_channels is not _UNDEF: + if min_channels is not cv.UNDEFINED: config[CONF_MIN_CHANNELS] = min_channels - if max_channels is not _UNDEF: + if max_channels is not cv.UNDEFINED: config[CONF_MAX_CHANNELS] = max_channels - if min_sample_rate is not _UNDEF: + if min_sample_rate is not cv.UNDEFINED: config[CONF_MIN_SAMPLE_RATE] = min_sample_rate - if max_sample_rate is not _UNDEF: + if max_sample_rate is not cv.UNDEFINED: config[CONF_MAX_SAMPLE_RATE] = max_sample_rate return set_limits_in_config @@ -75,10 +72,10 @@ def final_validate_audio_schema( name: str, *, audio_device: str, - bits_per_sample: int = _UNDEF, - channels: int = _UNDEF, - sample_rate: int = _UNDEF, - enabled_channels: list[int] = _UNDEF, + bits_per_sample: int = cv.UNDEFINED, + channels: int = cv.UNDEFINED, + sample_rate: int = cv.UNDEFINED, + enabled_channels: list[int] = cv.UNDEFINED, audio_device_issue: bool = False, ): """Validates audio compatibility when passed between different components. @@ -101,7 +98,7 @@ def final_validate_audio_schema( def validate_audio_compatiblity(audio_config): audio_schema = {} - if bits_per_sample is not _UNDEF: + if bits_per_sample is not cv.UNDEFINED: try: cv.int_range( min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE), @@ -114,7 +111,7 @@ def final_validate_audio_schema( error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}" raise cv.Invalid(error_string) from exc - if channels is not _UNDEF: + if channels is not cv.UNDEFINED: try: cv.int_range( min=audio_config.get(CONF_MIN_CHANNELS), @@ -127,7 +124,7 @@ def final_validate_audio_schema( error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}" raise cv.Invalid(error_string) from exc - if sample_rate is not _UNDEF: + if sample_rate is not cv.UNDEFINED: try: cv.int_range( min=audio_config.get(CONF_MIN_SAMPLE_RATE), @@ -140,7 +137,7 @@ def final_validate_audio_schema( error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}" raise cv.Invalid(error_string) from exc - if enabled_channels is not _UNDEF: + if enabled_channels is not cv.UNDEFINED: for channel in enabled_channels: try: # Channels are 0-indexed diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index d947c2aba6..d0fed9a9b8 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -458,19 +458,17 @@ BINARY_SENSOR_SCHEMA = ( ) ) -_UNDEF = object() - def binary_sensor_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass = cv.UNDEFINED, *, - icon: str = _UNDEF, - entity_category: str = _UNDEF, - device_class: str = _UNDEF, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, ) -> cv.Schema: schema = {} - if class_ is not _UNDEF: + if class_ is not cv.UNDEFINED: # Not cv.optional schema[cv.GenerateID()] = cv.declare_id(class_) @@ -479,7 +477,7 @@ def binary_sensor_schema( (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_DEVICE_CLASS, device_class, validate_device_class), ]: - if default is not _UNDEF: + if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator return BINARY_SENSOR_SCHEMA.extend(schema) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 366d0edf7d..0307fd3f03 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -60,15 +60,13 @@ BUTTON_SCHEMA = ( ) ) -_UNDEF = object() - def button_schema( class_: MockObjClass, *, - icon: str = _UNDEF, - entity_category: str = _UNDEF, - device_class: str = _UNDEF, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, ) -> cv.Schema: schema = {cv.GenerateID(): cv.declare_id(class_)} @@ -77,7 +75,7 @@ def button_schema( (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_DEVICE_CLASS, device_class, validate_device_class), ]: - if default is not _UNDEF: + if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator return BUTTON_SCHEMA.extend(schema) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index a7732dfcaf..99a698e8eb 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -58,19 +58,17 @@ EVENT_SCHEMA = ( ) ) -_UNDEF = object() - def event_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass = cv.UNDEFINED, *, - icon: str = _UNDEF, - entity_category: str = _UNDEF, - device_class: str = _UNDEF, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, ) -> cv.Schema: schema = {} - if class_ is not _UNDEF: + if class_ is not cv.UNDEFINED: schema[cv.GenerateID()] = cv.declare_id(class_) for key, default, validator in [ @@ -78,7 +76,7 @@ def event_schema( (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_DEVICE_CLASS, device_class, validate_device_class), ]: - if default is not _UNDEF: + if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator return EVENT_SCHEMA.extend(schema) diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index f85f0b76f3..2fda99af05 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -123,11 +123,8 @@ def microphone_source_schema( ) -_UNDEF = object() - - def final_validate_microphone_source_schema( - component_name: str, sample_rate: int = _UNDEF + component_name: str, sample_rate: int = cv.UNDEFINED ): """Validates that the microphone source can provide audio in the correct format. In particular it validates the sample rate and the enabled channels. @@ -141,7 +138,7 @@ def final_validate_microphone_source_schema( """ def _validate_audio_compatability(config): - if sample_rate is not _UNDEF: + if sample_rate is not cv.UNDEFINED: # Issues require changing the microphone configuration # - Verifies sample rates match audio.final_validate_audio_schema( diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index f45cfd54f2..0f68065805 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -196,16 +196,14 @@ NUMBER_SCHEMA = ( ) ) -_UNDEF = object() - def number_schema( class_: MockObjClass, *, - icon: str = _UNDEF, - entity_category: str = _UNDEF, - device_class: str = _UNDEF, - unit_of_measurement: str = _UNDEF, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, + unit_of_measurement: str = cv.UNDEFINED, ) -> cv.Schema: schema = {cv.GenerateID(): cv.declare_id(class_)} @@ -215,7 +213,7 @@ def number_schema( (CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_UNIT_OF_MEASUREMENT, unit_of_measurement, validate_unit_of_measurement), ]: - if default is not _UNDEF: + if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator return NUMBER_SCHEMA.extend(schema) diff --git a/esphome/components/opentherm/binary_sensor/__init__.py b/esphome/components/opentherm/binary_sensor/__init__.py index d4c7861a1d..ce5a701a53 100644 --- a/esphome/components/opentherm/binary_sensor/__init__.py +++ b/esphome/components/opentherm/binary_sensor/__init__.py @@ -12,9 +12,9 @@ COMPONENT_TYPE = const.BINARY_SENSOR def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema: return binary_sensor.binary_sensor_schema( device_class=( - entity.device_class or binary_sensor._UNDEF # pylint: disable=protected-access + entity.device_class or cv.UNDEFINED # pylint: disable=protected-access ), - icon=(entity.icon or binary_sensor._UNDEF), # pylint: disable=protected-access + icon=(entity.icon or cv.UNDEFINED), # pylint: disable=protected-access ) diff --git a/esphome/components/opentherm/sensor/__init__.py b/esphome/components/opentherm/sensor/__init__.py index 86c842b299..9aa33f457d 100644 --- a/esphome/components/opentherm/sensor/__init__.py +++ b/esphome/components/opentherm/sensor/__init__.py @@ -23,10 +23,10 @@ MSG_DATA_TYPES = { def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: return sensor.sensor_schema( - unit_of_measurement=entity.unit_of_measurement or sensor._UNDEF, # pylint: disable=protected-access + unit_of_measurement=entity.unit_of_measurement or cv.UNDEFINED, # pylint: disable=protected-access accuracy_decimals=entity.accuracy_decimals, - device_class=entity.device_class or sensor._UNDEF, # pylint: disable=protected-access - icon=entity.icon or sensor._UNDEF, # pylint: disable=protected-access + device_class=entity.device_class or cv.UNDEFINED, # pylint: disable=protected-access + icon=entity.icon or cv.UNDEFINED, # pylint: disable=protected-access state_class=entity.state_class, ).extend( { diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 5a3271fdfd..106571d94d 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -64,19 +64,17 @@ SELECT_SCHEMA = ( ) ) -_UNDEF = object() - def select_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass = cv.UNDEFINED, *, - entity_category: str = _UNDEF, - icon: str = _UNDEF, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, ): schema = cv.Schema({}) - if class_ is not _UNDEF: + if class_ is not cv.UNDEFINED: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if entity_category is not _UNDEF: + if entity_category is not cv.UNDEFINED: schema = schema.extend( { cv.Optional( @@ -84,7 +82,7 @@ def select_schema( ): cv.entity_category } ) - if icon is not _UNDEF: + if icon is not cv.UNDEFINED: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) return SELECT_SCHEMA.extend(schema) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 5f990466c8..1eb2d67d6e 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -309,22 +309,20 @@ SENSOR_SCHEMA = ( ) ) -_UNDEF = object() - def sensor_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass = cv.UNDEFINED, *, - unit_of_measurement: str = _UNDEF, - icon: str = _UNDEF, - accuracy_decimals: int = _UNDEF, - device_class: str = _UNDEF, - state_class: str = _UNDEF, - entity_category: str = _UNDEF, + unit_of_measurement: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, + accuracy_decimals: int = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, + state_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, ) -> cv.Schema: schema = {} - if class_ is not _UNDEF: + if class_ is not cv.UNDEFINED: # Not optional. schema[cv.GenerateID()] = cv.declare_id(class_) @@ -336,7 +334,7 @@ def sensor_schema( (CONF_STATE_CLASS, state_class, validate_state_class), (CONF_ENTITY_CATEGORY, entity_category, sensor_entity_category), ]: - if default is not _UNDEF: + if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator return SENSOR_SCHEMA.extend(schema) @@ -811,7 +809,9 @@ async def setup_sensor_core_(var, config): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if (expire_after := config.get(CONF_EXPIRE_AFTER, _UNDEF)) is not _UNDEF: + if ( + expire_after := config.get(CONF_EXPIRE_AFTER, cv.UNDEFINED) + ) is not cv.UNDEFINED: if expire_after is None: cg.add(mqtt_.disable_expire_after()) else: diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 0f159f69ec..fa69cf1440 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -87,15 +87,13 @@ _SWITCH_SCHEMA = ( ) ) -_UNDEF = object() - def switch_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass = cv.UNDEFINED, *, - entity_category: str = _UNDEF, - device_class: str = _UNDEF, - icon: str = _UNDEF, + entity_category: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, block_inverted: bool = False, default_restore_mode: str = "ALWAYS_OFF", ): @@ -106,9 +104,9 @@ def switch_schema( ), } ) - if class_ is not _UNDEF: + if class_ is not cv.UNDEFINED: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if entity_category is not _UNDEF: + if entity_category is not cv.UNDEFINED: schema = schema.extend( { cv.Optional( @@ -116,7 +114,7 @@ def switch_schema( ): cv.entity_category } ) - if device_class is not _UNDEF: + if device_class is not cv.UNDEFINED: schema = schema.extend( { cv.Optional( @@ -124,7 +122,7 @@ def switch_schema( ): validate_device_class } ) - if icon is not _UNDEF: + if icon is not cv.UNDEFINED: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) if block_inverted: schema = schema.extend( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 12993d9ffc..046af2bd26 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -152,22 +152,20 @@ TEXT_SENSOR_SCHEMA = ( ) ) -_UNDEF = object() - def text_sensor_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass = cv.UNDEFINED, *, - icon: str = _UNDEF, - entity_category: str = _UNDEF, - device_class: str = _UNDEF, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, ) -> cv.Schema: schema = TEXT_SENSOR_SCHEMA - if class_ is not _UNDEF: + if class_ is not cv.UNDEFINED: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if icon is not _UNDEF: + if icon is not cv.UNDEFINED: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - if device_class is not _UNDEF: + if device_class is not cv.UNDEFINED: schema = schema.extend( { cv.Optional( @@ -175,7 +173,7 @@ def text_sensor_schema( ): validate_device_class } ) - if entity_category is not _UNDEF: + if entity_category is not cv.UNDEFINED: schema = schema.extend( { cv.Optional( From bec9d914199035fe765fc8fdd746ced566eadca2 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 9 May 2025 16:54:33 -0500 Subject: [PATCH 031/193] [audio, microphone] - Allow MicrophoneSource to passively capture/optimization (#8732) --- esphome/components/audio/audio.h | 25 +++++++++++- esphome/components/microphone/__init__.py | 11 +++++- .../microphone/microphone_source.cpp | 38 ++++++------------- .../components/microphone/microphone_source.h | 9 +++-- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 2c556c68e2..95c31872e3 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -135,7 +135,7 @@ const char *audio_file_type_to_string(AudioFileType file_type); void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale); -/// @brief Unpacks a quantized audio sample into a Q31 fixed point number. +/// @brief Unpacks a quantized audio sample into a Q31 fixed-point number. /// @param data Pointer to uint8_t array containing the audio sample /// @param bytes_per_sample The number of bytes per sample /// @return Q31 sample @@ -160,5 +160,28 @@ inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_ return sample; } +/// @brief Packs a Q31 fixed-point number as an audio sample with the specified number of bytes per sample. +/// Packs the most significant bits - no dithering is applied. +/// @param sample Q31 fixed-point number to pack +/// @param data Pointer to data array to store +/// @param bytes_per_sample The audio data's bytes per sample +inline void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes_per_sample) { + if (bytes_per_sample == 1) { + data[0] = static_cast(sample >> 24); + } else if (bytes_per_sample == 2) { + data[0] = static_cast(sample >> 16); + data[1] = static_cast(sample >> 24); + } else if (bytes_per_sample == 3) { + data[0] = static_cast(sample >> 8); + data[1] = static_cast(sample >> 16); + data[2] = static_cast(sample >> 24); + } else if (bytes_per_sample == 4) { + data[0] = static_cast(sample); + data[1] = static_cast(sample >> 8); + data[2] = static_cast(sample >> 16); + data[3] = static_cast(sample >> 24); + } +} + } // namespace audio } // namespace esphome diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 2fda99af05..29bdcfa3f3 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -162,13 +162,22 @@ def final_validate_microphone_source_schema( return _validate_audio_compatability -async def microphone_source_to_code(config): +async def microphone_source_to_code(config, passive=False): + """Creates a MicrophoneSource variable for codegen. + + Setting passive to true makes the MicrophoneSource never start/stop the microphone, but only receives audio when another component has actively started the Microphone. If false, then the microphone needs to be explicitly started/stopped. + + Args: + config (Schema): Created with `microphone_source_schema` specifying bits per sample, channels, and gain factor + passive (bool): Enable passive mode for the MicrophoneSource + """ mic = await cg.get_variable(config[CONF_MICROPHONE]) mic_source = cg.new_Pvariable( config[CONF_ID], mic, config[CONF_BITS_PER_SAMPLE], config[CONF_GAIN_FACTOR], + passive, ) for channel in config[CONF_CHANNELS]: cg.add(mic_source.add_channel(channel)) diff --git a/esphome/components/microphone/microphone_source.cpp b/esphome/components/microphone/microphone_source.cpp index 1ea0deb22b..00efcf22a1 100644 --- a/esphome/components/microphone/microphone_source.cpp +++ b/esphome/components/microphone/microphone_source.cpp @@ -6,12 +6,10 @@ namespace microphone { static const int32_t Q25_MAX_VALUE = (1 << 25) - 1; static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE; -static const uint32_t HISTORY_VALUES = 32; - void MicrophoneSource::add_data_callback(std::function &)> &&data_callback) { std::function &)> filtered_callback = [this, data_callback](const std::vector &data) { - if (this->enabled_) { + if (this->enabled_ || this->passive_) { if (this->processed_samples_.use_count() == 0) { // Create vector if its unused this->processed_samples_ = std::make_shared>(); @@ -32,13 +30,14 @@ audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() { } void MicrophoneSource::start() { - if (!this->enabled_) { + if (!this->enabled_ && !this->passive_) { this->enabled_ = true; this->mic_->start(); } } + void MicrophoneSource::stop() { - if (this->enabled_) { + if (this->enabled_ && !this->passive_) { this->enabled_ = false; this->mic_->stop(); this->processed_samples_.reset(); @@ -63,8 +62,9 @@ void MicrophoneSource::process_audio_(const std::vector &data, std::vec const size_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8; const size_t target_bytes_per_frame = target_bytes_per_sample * this->channels_.count(); - filtered_data.reserve(target_bytes_per_frame * total_frames); - filtered_data.resize(0); + filtered_data.resize(target_bytes_per_frame * total_frames); + + uint8_t *current_data = filtered_data.data(); for (uint32_t frame_index = 0; frame_index < total_frames; ++frame_index) { for (uint32_t channel_index = 0; channel_index < source_channels; ++channel_index) { @@ -82,26 +82,10 @@ void MicrophoneSource::process_audio_(const std::vector &data, std::vec // Clamp ``sample`` in case gain multiplication overflows 25 bits sample = clamp(sample, Q25_MIN_VALUE, Q25_MAX_VALUE); // Q25 - // Copy ``target_bytes_per_sample`` bytes to the output buffer. - if (target_bytes_per_sample == 1) { - sample >>= 18; // Q25 -> Q7 - filtered_data.push_back(static_cast(sample)); - } else if (target_bytes_per_sample == 2) { - sample >>= 10; // Q25 -> Q15 - filtered_data.push_back(static_cast(sample)); - filtered_data.push_back(static_cast(sample >> 8)); - } else if (target_bytes_per_sample == 3) { - sample >>= 2; // Q25 -> Q23 - filtered_data.push_back(static_cast(sample)); - filtered_data.push_back(static_cast(sample >> 8)); - filtered_data.push_back(static_cast(sample >> 16)); - } else { - sample *= (1 << 6); // Q25 -> Q31 - filtered_data.push_back(static_cast(sample)); - filtered_data.push_back(static_cast(sample >> 8)); - filtered_data.push_back(static_cast(sample >> 16)); - filtered_data.push_back(static_cast(sample >> 24)); - } + sample *= (1 << 6); // Q25 -> Q31 + + audio::pack_q31_as_audio_sample(sample, current_data, target_bytes_per_sample); + current_data = current_data + target_bytes_per_sample; } } } diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index 7f8a37b360..228f2d9dc3 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -35,8 +35,8 @@ class MicrophoneSource { * Note that this class cannot convert sample rates! */ public: - MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor) - : mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor) {} + MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor, bool passive) + : mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor), passive_(passive) {} /// @brief Enables a channel to be processed through the callback. /// @@ -59,8 +59,8 @@ class MicrophoneSource { void start(); void stop(); - bool is_running() const { return (this->mic_->is_running() && this->enabled_); } - bool is_stopped() const { return !this->enabled_; } + bool is_running() const { return (this->mic_->is_running() && (this->enabled_ || this->passive_)); } + bool is_stopped() const { return !this->is_running(); }; protected: void process_audio_(const std::vector &data, std::vector &filtered_data); @@ -72,6 +72,7 @@ class MicrophoneSource { std::bitset<8> channels_; int32_t gain_factor_; bool enabled_{false}; + bool passive_{false}; }; } // namespace microphone From ff1c3cb52e52ba0809ceb49528c140a815b1883b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 11 May 2025 00:25:19 -0500 Subject: [PATCH 032/193] [audio] Bump esp-audio-libs to version 1.1.4 for speed optimizations (#8739) --- esphome/components/audio/__init__.py | 2 +- platformio.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 06f3cf09d8..f657cb5da3 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -165,4 +165,4 @@ def final_validate_audio_schema( async def to_code(config): - cg.add_library("esphome/esp-audio-libs", "1.1.3") + cg.add_library("esphome/esp-audio-libs", "1.1.4") diff --git a/platformio.ini b/platformio.ini index a2d5d27faf..61b7f8d746 100644 --- a/platformio.ini +++ b/platformio.ini @@ -128,7 +128,7 @@ lib_deps = DNSServer ; captive_portal (Arduino built-in) esphome/ESP32-audioI2S@2.0.7 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard - esphome/esp-audio-libs@1.1.3 ; audio + esphome/esp-audio-libs@1.1.4 ; audio build_flags = ${common:arduino.build_flags} @@ -149,7 +149,7 @@ lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.2 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word - esphome/esp-audio-libs@1.1.3 ; audio + esphome/esp-audio-libs@1.1.4 ; audio build_flags = ${common:idf.build_flags} -Wno-nonnull-compare From cdce59f7f95f0c13e997d73a5784d2a41bf0fc9e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 11 May 2025 00:27:50 -0500 Subject: [PATCH 033/193] [i2s_audio] Fix: Slot bit-width for ESP32 variant (#8738) --- .../components/i2s_audio/speaker/i2s_audio_speaker.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index b287177016..d85409f1a8 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -629,7 +629,16 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea std_slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); } +#ifdef USE_ESP32_VARIANT_ESP32 + // There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher then the bits + // per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to + // make it play at the correct speed while sending more bits per slot. + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { + std_slot_cfg.ws_width = static_cast(this->slot_bit_width_); + } +#else std_slot_cfg.slot_bit_width = this->slot_bit_width_; +#endif std_slot_cfg.slot_mask = slot_mask; pin_config.dout = this->dout_pin_; From 7f59aff1576d46c954d31da3e5baa19cad44a05c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 11 May 2025 15:50:47 -0500 Subject: [PATCH 034/193] [voice_assistant] Bugfix: Properly detect states where mic is running (#8745) --- .../components/voice_assistant/voice_assistant.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d35717ef91..1aafea7d85 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -695,12 +695,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_RUN_END: { ESP_LOGD(TAG, "Assist Pipeline ended"); - if ((this->state_ == State::STARTING_PIPELINE) || (this->state_ == State::AWAITING_RESPONSE)) { - // Pipeline ended before starting microphone - // Or there wasn't a TTS start event ("nevermind") - this->set_state_(State::IDLE, State::IDLE); - } else if (this->state_ == State::STREAMING_MICROPHONE) { - this->ring_buffer_->reset(); + if ((this->state_ == State::START_PIPELINE) || (this->state_ == State::STARTING_PIPELINE) || + (this->state_ == State::STREAMING_MICROPHONE)) { + // Microphone is running, stop it + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + } else if (this->state_ == State::AWAITING_RESPONSE) { + // No TTS start event ("nevermind") this->set_state_(State::IDLE, State::IDLE); } this->defer([this]() { this->end_trigger_->trigger(); }); From cdc1a7c64635690ab496c285d27d3ab083680244 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 11 May 2025 15:51:49 -0500 Subject: [PATCH 035/193] [sound_level] Add a new sound level sensor (#8737) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/microphone/microphone_source.h | 3 +- esphome/components/sound_level/__init__.py | 0 esphome/components/sound_level/sensor.py | 97 +++++++++ .../components/sound_level/sound_level.cpp | 194 ++++++++++++++++++ esphome/components/sound_level/sound_level.h | 73 +++++++ tests/components/sound_level/common.yaml | 26 +++ .../sound_level/test.esp32-ard.yaml | 6 + .../sound_level/test.esp32-c3-ard.yaml | 6 + .../sound_level/test.esp32-c3-idf.yaml | 6 + .../sound_level/test.esp32-idf.yaml | 6 + .../sound_level/test.esp32-s3-ard.yaml | 6 + .../sound_level/test.esp32-s3-idf.yaml | 6 + 13 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 esphome/components/sound_level/__init__.py create mode 100644 esphome/components/sound_level/sensor.py create mode 100644 esphome/components/sound_level/sound_level.cpp create mode 100644 esphome/components/sound_level/sound_level.h create mode 100644 tests/components/sound_level/common.yaml create mode 100644 tests/components/sound_level/test.esp32-ard.yaml create mode 100644 tests/components/sound_level/test.esp32-c3-ard.yaml create mode 100644 tests/components/sound_level/test.esp32-c3-idf.yaml create mode 100644 tests/components/sound_level/test.esp32-idf.yaml create mode 100644 tests/components/sound_level/test.esp32-s3-ard.yaml create mode 100644 tests/components/sound_level/test.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 29919b6d70..e6c149012a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -398,6 +398,7 @@ esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov +esphome/components/sound_level/* @kahrendt esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/spi/* @clydebarrow @esphome/core diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index 228f2d9dc3..1e81a284b6 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -59,6 +59,7 @@ class MicrophoneSource { void start(); void stop(); + bool is_passive() const { return this->passive_; } bool is_running() const { return (this->mic_->is_running() && (this->enabled_ || this->passive_)); } bool is_stopped() const { return !this->is_running(); }; @@ -72,7 +73,7 @@ class MicrophoneSource { std::bitset<8> channels_; int32_t gain_factor_; bool enabled_{false}; - bool passive_{false}; + bool passive_; // Only pass audio if ``mic_`` is already running }; } // namespace microphone diff --git a/esphome/components/sound_level/__init__.py b/esphome/components/sound_level/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py new file mode 100644 index 0000000000..292efadab8 --- /dev/null +++ b/esphome/components/sound_level/sensor.py @@ -0,0 +1,97 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import microphone, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MEASUREMENT_DURATION, + CONF_MICROPHONE, + DEVICE_CLASS_SOUND_PRESSURE, + PLATFORM_ESP32, + STATE_CLASS_MEASUREMENT, + UNIT_DECIBEL, +) + +AUTOLOAD = ["audio"] +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["microphone"] + + +CONF_PASSIVE = "passive" +CONF_PEAK = "peak" +CONF_RMS = "rms" + +sound_level_ns = cg.esphome_ns.namespace("sound_level") +SoundLevelComponent = sound_level_ns.class_("SoundLevelComponent", cg.Component) + +StartAction = sound_level_ns.class_("StartAction", automation.Action) +StopAction = sound_level_ns.class_("StopAction", automation.Action) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SoundLevelComponent), + cv.Optional(CONF_MEASUREMENT_DURATION, default="1000ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=cv.TimePeriod(milliseconds=50), + max=cv.TimePeriod(seconds=60), + ), + ), + cv.Optional( + CONF_MICROPHONE, default={} + ): microphone.microphone_source_schema( + min_bits_per_sample=16, + max_bits_per_sample=16, + ), + cv.Required(CONF_PASSIVE): cv.boolean, + cv.Optional(CONF_PEAK): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_SOUND_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RMS): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_SOUND_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP32]), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + mic_source = await microphone.microphone_source_to_code( + config[CONF_MICROPHONE], passive=config[CONF_PASSIVE] + ) + cg.add(var.set_microphone_source(mic_source)) + + cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION])) + + if peak_config := config.get(CONF_PEAK): + sens = await sensor.new_sensor(peak_config) + cg.add(var.set_peak_sensor(sens)) + if rms_config := config.get(CONF_RMS): + sens = await sensor.new_sensor(rms_config) + cg.add(var.set_rms_sensor(sens)) + + +SOUND_LEVEL_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SoundLevelComponent), + } +) + + +@automation.register_action("sound_level.start", StartAction, SOUND_LEVEL_ACTION_SCHEMA) +@automation.register_action("sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA) +async def sound_level_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp new file mode 100644 index 0000000000..f8447ce436 --- /dev/null +++ b/esphome/components/sound_level/sound_level.cpp @@ -0,0 +1,194 @@ +#include "sound_level.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +#include +#include + +namespace esphome { +namespace sound_level { + +static const char *const TAG = "sound_level"; + +static const uint32_t AUDIO_BUFFER_DURATION_MS = 30; +static const uint32_t RING_BUFFER_DURATION_MS = 120; + +// Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2 +static const double MAX_SAMPLE_SQUARED_DENOMINATOR = INT16_MIN * INT16_MIN; + +void SoundLevelComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Sound Level Component:"); + ESP_LOGCONFIG(TAG, " Measurement Duration: %" PRIu32 " ms", measurement_duration_ms_); + LOG_SENSOR(" ", "Peak:", this->peak_sensor_); + + LOG_SENSOR(" ", "RMS:", this->rms_sensor_); +} + +void SoundLevelComponent::setup() { + this->microphone_source_->add_data_callback([this](const std::vector &data) { + std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); + if (this->ring_buffer_.use_count() == 2) { + // ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write + temp_ring_buffer->write((void *) data.data(), data.size()); + } + }); + + if (!this->microphone_source_->is_passive()) { + // Automatically start the microphone if not in passive mode + this->microphone_source_->start(); + } +} + +void SoundLevelComponent::loop() { + if ((this->peak_sensor_ == nullptr) && (this->rms_sensor_ == nullptr)) { + // No sensors configured, nothing to do + return; + } + + if (this->microphone_source_->is_running() && !this->status_has_error()) { + // Allocate buffers + if (this->start_()) { + this->status_clear_warning(); + } + } else { + if (!this->status_has_warning()) { + this->status_set_warning("Microphone isn't running, can't compute statistics"); + + // Deallocate buffers, if necessary + this->stop_(); + + // Reset sensor outputs + if (this->peak_sensor_ != nullptr) { + this->peak_sensor_->publish_state(NAN); + } + if (this->rms_sensor_ != nullptr) { + this->rms_sensor_->publish_state(NAN); + } + + // Reset accumulators + this->squared_peak_ = 0; + this->squared_samples_sum_ = 0; + this->sample_count_ = 0; + } + + return; + } + + if (this->status_has_error()) { + return; + } + + // Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop + this->audio_buffer_->transfer_data_from_source(0); + + if (this->audio_buffer_->available() == 0) { + // No new audio available for processing + return; + } + + const uint32_t samples_in_window = + this->microphone_source_->get_audio_stream_info().ms_to_samples(this->measurement_duration_ms_); + const uint32_t samples_available_to_process = + this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_buffer_->available()); + const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process); + + // MicrophoneSource always provides int16 samples due to Python codegen settings + const int16_t *audio_data = reinterpret_cast(this->audio_buffer_->get_buffer_start()); + + // Process all the new audio samples + for (uint32_t i = 0; i < samples_to_process; ++i) { + // Squaring int16 samples won't overflow an int32 + int32_t squared_sample = static_cast(audio_data[i]) * static_cast(audio_data[i]); + + if (this->peak_sensor_ != nullptr) { + this->squared_peak_ = std::max(this->squared_peak_, squared_sample); + } + + if (this->rms_sensor_ != nullptr) { + // Squared sum is an uint64 type - at max levels, an uint32 type would overflow after ~8 samples + this->squared_samples_sum_ += squared_sample; + } + + ++this->sample_count_; + } + + // Remove the processed samples from ``audio_buffer_`` + this->audio_buffer_->decrease_buffer_length( + this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); + + if (this->sample_count_ == samples_in_window) { + // Processed enough samples for the measurement window, compute and publish the sensor values + if (this->peak_sensor_ != nullptr) { + const float peak_db = 10.0f * log10(static_cast(this->squared_peak_) / MAX_SAMPLE_SQUARED_DENOMINATOR); + this->peak_sensor_->publish_state(peak_db); + + this->squared_peak_ = 0; // reset accumulator + } + + if (this->rms_sensor_ != nullptr) { + // Calculations are done with doubles instead of floats - floats lose precision for even modest window durations + const double rms_db = 10.0 * log10((this->squared_samples_sum_ / MAX_SAMPLE_SQUARED_DENOMINATOR) / + static_cast(samples_in_window)); + this->rms_sensor_->publish_state(rms_db); + + this->squared_samples_sum_ = 0; // reset accumulator + } + + this->sample_count_ = 0; // reset counter + } +} + +void SoundLevelComponent::start() { + if (this->microphone_source_->is_passive()) { + ESP_LOGW(TAG, "Can't start the microphone in passive mode"); + return; + } + this->microphone_source_->start(); +} + +void SoundLevelComponent::stop() { + if (this->microphone_source_->is_passive()) { + ESP_LOGW(TAG, "Can't stop microphone in passive mode"); + return; + } + this->microphone_source_->stop(); +} + +bool SoundLevelComponent::start_() { + if (this->audio_buffer_ != nullptr) { + return true; + } + + // Allocate a transfer buffer + this->audio_buffer_ = audio::AudioSourceTransferBuffer::create( + this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS)); + if (this->audio_buffer_ == nullptr) { + this->status_momentary_error("Failed to allocate transfer buffer", 15000); + return false; + } + + // Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it + this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation + std::shared_ptr temp_ring_buffer = + RingBuffer::create(this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); + if (temp_ring_buffer.use_count() == 0) { + this->status_momentary_error("Failed to allocate ring buffer", 15000); + this->stop_(); + return false; + } else { + this->ring_buffer_ = temp_ring_buffer; + this->audio_buffer_->set_source(temp_ring_buffer); + } + + this->status_clear_error(); + return true; +} + +void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); } + +} // namespace sound_level +} // namespace esphome + +#endif diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h new file mode 100644 index 0000000000..6a80a60ac7 --- /dev/null +++ b/esphome/components/sound_level/sound_level.h @@ -0,0 +1,73 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio_transfer_buffer.h" +#include "esphome/components/microphone/microphone_source.h" +#include "esphome/components/sensor/sensor.h" + +#include "esphome/core/component.h" +#include "esphome/core/ring_buffer.h" + +namespace esphome { +namespace sound_level { + +class SoundLevelComponent : public Component { + public: + void dump_config() override; + void setup() override; + void loop() override; + + float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } + + void set_measurement_duration(uint32_t measurement_duration_ms) { + this->measurement_duration_ms_ = measurement_duration_ms; + } + void set_microphone_source(microphone::MicrophoneSource *microphone_source) { + this->microphone_source_ = microphone_source; + } + void set_peak_sensor(sensor::Sensor *peak_sensor) { this->peak_sensor_ = peak_sensor; } + void set_rms_sensor(sensor::Sensor *rms_sensor) { this->rms_sensor_ = rms_sensor; } + + /// @brief Starts the MicrophoneSource to start measuring sound levels + void start(); + + /// @brief Stops the MicrophoneSource + void stop(); + + protected: + /// @brief Internal start command that, if necessary, allocates ``audio_buffer_`` and a ring buffer which + /// ``audio_buffer_`` owns and ``ring_buffer_`` points to. Returns true if allocations were successful. + bool start_(); + + /// @brief Internal stop command the deallocates ``audio_buffer_`` (which automatically deallocates its ring buffer) + void stop_(); + + microphone::MicrophoneSource *microphone_source_{nullptr}; + + sensor::Sensor *peak_sensor_{nullptr}; + sensor::Sensor *rms_sensor_{nullptr}; + + std::unique_ptr audio_buffer_; + std::weak_ptr ring_buffer_; + + int32_t squared_peak_{0}; + uint64_t squared_samples_sum_{0}; + uint32_t sample_count_{0}; + + uint32_t measurement_duration_ms_; +}; + +template class StartAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->start(); } +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +} // namespace sound_level +} // namespace esphome +#endif diff --git a/tests/components/sound_level/common.yaml b/tests/components/sound_level/common.yaml new file mode 100644 index 0000000000..cc04f5bf79 --- /dev/null +++ b/tests/components/sound_level/common.yaml @@ -0,0 +1,26 @@ +i2s_audio: + i2s_lrclk_pin: ${i2s_bclk_pin} + i2s_bclk_pin: ${i2s_lrclk_pin} + +microphone: + - platform: i2s_audio + id: i2s_microphone + i2s_din_pin: ${i2s_dout_pin} + adc_type: external + bits_per_sample: 16bit + +sensor: + - platform: sound_level + microphone: i2s_microphone + measurement_duration: 2000ms + passive: false + peak: + name: "Peak Sound Level" + on_value_range: + - above: -1.0 + then: + - sound_level.stop: + - delay: 5s + - sound_level.start: + rms: + name: "RMS Sound Level" diff --git a/tests/components/sound_level/test.esp32-ard.yaml b/tests/components/sound_level/test.esp32-ard.yaml new file mode 100644 index 0000000000..c6d1bfa330 --- /dev/null +++ b/tests/components/sound_level/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + i2s_bclk_pin: GPIO25 + i2s_lrclk_pin: GPIO26 + i2s_dout_pin: GPIO27 + +<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-c3-ard.yaml b/tests/components/sound_level/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..aeb7d9f0af --- /dev/null +++ b/tests/components/sound_level/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + i2s_bclk_pin: GPIO6 + i2s_lrclk_pin: GPIO7 + i2s_dout_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-c3-idf.yaml b/tests/components/sound_level/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..aeb7d9f0af --- /dev/null +++ b/tests/components/sound_level/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + i2s_bclk_pin: GPIO6 + i2s_lrclk_pin: GPIO7 + i2s_dout_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-idf.yaml b/tests/components/sound_level/test.esp32-idf.yaml new file mode 100644 index 0000000000..c6d1bfa330 --- /dev/null +++ b/tests/components/sound_level/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + i2s_bclk_pin: GPIO25 + i2s_lrclk_pin: GPIO26 + i2s_dout_pin: GPIO27 + +<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-s3-ard.yaml b/tests/components/sound_level/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..9c1f32d5bd --- /dev/null +++ b/tests/components/sound_level/test.esp32-s3-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + i2s_bclk_pin: GPIO4 + i2s_lrclk_pin: GPIO5 + i2s_dout_pin: GPIO6 + +<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-s3-idf.yaml b/tests/components/sound_level/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..9c1f32d5bd --- /dev/null +++ b/tests/components/sound_level/test.esp32-s3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + i2s_bclk_pin: GPIO4 + i2s_lrclk_pin: GPIO5 + i2s_dout_pin: GPIO6 + +<<: !include common.yaml From a96ed0b70aae9746b573f36688e26acfdd11c4e1 Mon Sep 17 00:00:00 2001 From: bdm310 Date: Tue, 22 Apr 2025 03:13:43 -0700 Subject: [PATCH 036/193] [lvgl] Fix unexpected widget update behavior (#8260) --- esphome/components/lvgl/widgets/arc.py | 13 +++++++------ esphome/components/lvgl/widgets/dropdown.py | 3 ++- esphome/components/lvgl/widgets/img.py | 11 ++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index dc120e4cbb..65f0e785b6 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -67,12 +67,13 @@ class ArcType(NumberType): lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) - if config.get(CONF_ADJUSTABLE) is False: - lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) - w.clear_flag("LV_OBJ_FLAG_CLICKABLE") - elif CONF_GROUP not in config: - # For some reason arc does not get automatically added to the default group - lv.group_add_obj(lv_expr.group_get_default(), w.obj) + if CONF_ADJUSTABLE in config: + if not config[CONF_ADJUSTABLE]: + lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) + w.clear_flag("LV_OBJ_FLAG_CLICKABLE") + elif CONF_GROUP not in config: + # For some reason arc does not get automatically added to the default group + lv.group_add_obj(lv_expr.group_get_default(), w.obj) value = await get_start_value(config) if value is not None: diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py index b32b5a2b2e..9ff183f3dd 100644 --- a/esphome/components/lvgl/widgets/dropdown.py +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -36,7 +36,6 @@ DROPDOWN_BASE_SCHEMA = cv.Schema( cv.Optional(CONF_SYMBOL): lv_text, cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int, cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text, - cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts), } ) @@ -44,12 +43,14 @@ DROPDOWN_BASE_SCHEMA = cv.Schema( DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( { cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, } ) DROPDOWN_UPDATE_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( { cv.Optional(CONF_OPTIONS): cv.ensure_list(option_string), + cv.Optional(CONF_DIR): DIRECTIONS.one_of, } ) diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index d9de8d821a..c3e0781489 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -20,8 +20,8 @@ CONF_IMAGE = "image" BASE_IMG_SCHEMA = cv.Schema( { - cv.Optional(CONF_PIVOT_X, default="50%"): size, - cv.Optional(CONF_PIVOT_Y, default="50%"): size, + cv.Optional(CONF_PIVOT_X): size, + cv.Optional(CONF_PIVOT_Y): size, cv.Optional(CONF_ANGLE): angle, cv.Optional(CONF_ZOOM): zoom, cv.Optional(CONF_OFFSET_X): size, @@ -63,10 +63,11 @@ class ImgType(WidgetType): async def to_code(self, w: Widget, config): if src := config.get(CONF_SRC): lv.img_set_src(w.obj, await lv_image.process(src)) - if (cf_angle := config.get(CONF_ANGLE)) is not None: - pivot_x = config[CONF_PIVOT_X] - pivot_y = config[CONF_PIVOT_Y] + if (pivot_x := config.get(CONF_PIVOT_X)) and ( + pivot_y := config.get(CONF_PIVOT_Y) + ): lv.img_set_pivot(w.obj, pivot_x, pivot_y) + if (cf_angle := config.get(CONF_ANGLE)) is not None: lv.img_set_angle(w.obj, cf_angle) if (img_zoom := config.get(CONF_ZOOM)) is not None: lv.img_set_zoom(w.obj, img_zoom) From 290b8bdca0eb02e4066023e99f22383b520a4658 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:46:39 +1200 Subject: [PATCH 037/193] [esp32_ble] Remove explicit and now incorrect ble override for esp32-c6 (#8643) --- esphome/components/esp32_ble/ble.cpp | 8 --- esphome/components/esp32_ble/const_esp32c6.h | 74 -------------------- 2 files changed, 82 deletions(-) delete mode 100644 esphome/components/esp32_ble/const_esp32c6.h diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b10e454c21..ab2647b738 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -2,10 +2,6 @@ #include "ble.h" -#ifdef USE_ESP32_VARIANT_ESP32C6 -#include "const_esp32c6.h" -#endif // USE_ESP32_VARIANT_ESP32C6 - #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -127,11 +123,7 @@ bool ESP32BLE::ble_setup_() { if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { // start bt controller if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { -#ifdef USE_ESP32_VARIANT_ESP32C6 - esp_bt_controller_config_t cfg = BT_CONTROLLER_CONFIG; -#else esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); -#endif err = esp_bt_controller_init(&cfg); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); diff --git a/esphome/components/esp32_ble/const_esp32c6.h b/esphome/components/esp32_ble/const_esp32c6.h deleted file mode 100644 index 89179d8dd9..0000000000 --- a/esphome/components/esp32_ble/const_esp32c6.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#ifdef USE_ESP32_VARIANT_ESP32C6 - -#include - -namespace esphome { -namespace esp32_ble { - -static const esp_bt_controller_config_t BT_CONTROLLER_CONFIG = { - .config_version = CONFIG_VERSION, - .ble_ll_resolv_list_size = CONFIG_BT_LE_LL_RESOLV_LIST_SIZE, - .ble_hci_evt_hi_buf_count = DEFAULT_BT_LE_HCI_EVT_HI_BUF_COUNT, - .ble_hci_evt_lo_buf_count = DEFAULT_BT_LE_HCI_EVT_LO_BUF_COUNT, - .ble_ll_sync_list_cnt = DEFAULT_BT_LE_MAX_PERIODIC_ADVERTISER_LIST, - .ble_ll_sync_cnt = DEFAULT_BT_LE_MAX_PERIODIC_SYNCS, - .ble_ll_rsp_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT, - .ble_ll_adv_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT, - .ble_ll_tx_pwr_dbm = BLE_LL_TX_PWR_DBM_N, - .rtc_freq = RTC_FREQ_N, - .ble_ll_sca = CONFIG_BT_LE_LL_SCA, - .ble_ll_scan_phy_number = BLE_LL_SCAN_PHY_NUMBER_N, - .ble_ll_conn_def_auth_pyld_tmo = BLE_LL_CONN_DEF_AUTH_PYLD_TMO_N, - .ble_ll_jitter_usecs = BLE_LL_JITTER_USECS_N, - .ble_ll_sched_max_adv_pdu_usecs = BLE_LL_SCHED_MAX_ADV_PDU_USECS_N, - .ble_ll_sched_direct_adv_max_usecs = BLE_LL_SCHED_DIRECT_ADV_MAX_USECS_N, - .ble_ll_sched_adv_max_usecs = BLE_LL_SCHED_ADV_MAX_USECS_N, - .ble_scan_rsp_data_max_len = DEFAULT_BT_LE_SCAN_RSP_DATA_MAX_LEN_N, - .ble_ll_cfg_num_hci_cmd_pkts = BLE_LL_CFG_NUM_HCI_CMD_PKTS_N, - .ble_ll_ctrl_proc_timeout_ms = BLE_LL_CTRL_PROC_TIMEOUT_MS_N, - .nimble_max_connections = DEFAULT_BT_LE_MAX_CONNECTIONS, - .ble_whitelist_size = DEFAULT_BT_NIMBLE_WHITELIST_SIZE, // NOLINT - .ble_acl_buf_size = DEFAULT_BT_LE_ACL_BUF_SIZE, - .ble_acl_buf_count = DEFAULT_BT_LE_ACL_BUF_COUNT, - .ble_hci_evt_buf_size = DEFAULT_BT_LE_HCI_EVT_BUF_SIZE, - .ble_multi_adv_instances = DEFAULT_BT_LE_MAX_EXT_ADV_INSTANCES, - .ble_ext_adv_max_size = DEFAULT_BT_LE_EXT_ADV_MAX_SIZE, - .controller_task_stack_size = NIMBLE_LL_STACK_SIZE, - .controller_task_prio = ESP_TASK_BT_CONTROLLER_PRIO, - .controller_run_cpu = 0, - .enable_qa_test = RUN_QA_TEST, - .enable_bqb_test = RUN_BQB_TEST, -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 1) - // The following fields have been removed since ESP IDF version 5.3.1, see commit: - // https://github.com/espressif/esp-idf/commit/e761c1de8f9c0777829d597b4d5a33bb070a30a8 - .enable_uart_hci = HCI_UART_EN, - .ble_hci_uart_port = DEFAULT_BT_LE_HCI_UART_PORT, - .ble_hci_uart_baud = DEFAULT_BT_LE_HCI_UART_BAUD, - .ble_hci_uart_data_bits = DEFAULT_BT_LE_HCI_UART_DATA_BITS, - .ble_hci_uart_stop_bits = DEFAULT_BT_LE_HCI_UART_STOP_BITS, - .ble_hci_uart_flow_ctrl = DEFAULT_BT_LE_HCI_UART_FLOW_CTRL, - .ble_hci_uart_uart_parity = DEFAULT_BT_LE_HCI_UART_PARITY, -#endif - .enable_tx_cca = DEFAULT_BT_LE_TX_CCA_ENABLED, - .cca_rssi_thresh = 256 - DEFAULT_BT_LE_CCA_RSSI_THRESH, - .sleep_en = NIMBLE_SLEEP_ENABLE, - .coex_phy_coded_tx_rx_time_limit = DEFAULT_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_EFF, - .dis_scan_backoff = NIMBLE_DISABLE_SCAN_BACKOFF, - .ble_scan_classify_filter_enable = 1, - .main_xtal_freq = CONFIG_XTAL_FREQ, - .version_num = (uint8_t) efuse_hal_chip_revision(), - .cpu_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ, - .ignore_wl_for_direct_adv = 0, - .enable_pcl = DEFAULT_BT_LE_POWER_CONTROL_ENABLED, -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3) - .csa2_select = DEFAULT_BT_LE_50_FEATURE_SUPPORT, -#endif - .config_magic = CONFIG_MAGIC, -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32C6 From a85dc65038ba5d2de72b24d898afea3779105d77 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:08:46 +1200 Subject: [PATCH 038/193] [media_player] Fix actions with id as value (#8654) --- esphome/components/media_player/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index b2543ac05f..14fe1fdb6a 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -134,11 +134,13 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ) -MEDIA_PLAYER_ACTION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(MediaPlayer), - cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), - } +MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(MediaPlayer), + cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), + } + ) ) MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( From 82c8614315e94e869c043842f77988a73456a33b Mon Sep 17 00:00:00 2001 From: Ben Winslow Date: Wed, 30 Apr 2025 20:55:36 -0400 Subject: [PATCH 039/193] Fix typo preventing tt21100 from autosetting the touchscreen res. (#8662) --- esphome/components/tt21100/touchscreen/tt21100.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index 2bea72a59e..d18ce835c1 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -68,7 +68,7 @@ void TT21100Touchscreen::setup() { this->x_raw_max_ = this->display_->get_native_width(); } if (this->y_raw_max_ == this->y_raw_min_) { - this->x_raw_max_ = this->display_->get_native_height(); + this->y_raw_max_ = this->display_->get_native_height(); } } From 0c0fe8181490f2653236ac26f6dca6feb65eb202 Mon Sep 17 00:00:00 2001 From: Jannik <33796278+SuperPlusUser@users.noreply.github.com> Date: Thu, 1 May 2025 03:12:51 +0200 Subject: [PATCH 040/193] Fix HLW8012 sensor not returning values if change_mode_every is set to never (#8456) --- esphome/components/hlw8012/hlw8012.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 14e83f60e1..1efc57ab66 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -69,7 +69,7 @@ void HLW8012Component::update() { float power = cf_hz * this->power_multiplier_; - if (this->change_mode_at_ != 0) { + if (this->change_mode_at_ != 0 || this->change_mode_every_ == 0) { // Only read cf1 after one cycle. Apparently it's quite unstable after being changed. if (this->current_mode_) { float current = cf1_hz * this->current_multiplier_; From 2af3994f79f8dac893d61d0d7bd9ea5a5876247b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 May 2025 10:04:33 +1000 Subject: [PATCH 041/193] [display] Fix Rect::inside (#8679) --- esphome/components/display/rect.cpp | 13 ++++--------- esphome/components/display/rect.h | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp index 49bb7d025f..2c41127860 100644 --- a/esphome/components/display/rect.cpp +++ b/esphome/components/display/rect.cpp @@ -69,21 +69,16 @@ bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOL return true; } if (absolute) { - return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); - } else { - return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); + return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2(); } + return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h; } -bool Rect::inside(Rect rect, bool absolute) const { +bool Rect::inside(Rect rect) const { if (!this->is_set() || !rect.is_set()) { return true; } - if (absolute) { - return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); - } else { - return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); - } + return this->x2() >= rect.x && this->x <= rect.x2() && this->y2() >= rect.y && this->y <= rect.y2(); } void Rect::info(const std::string &prefix) { diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h index f55c2fe201..5f11d94681 100644 --- a/esphome/components/display/rect.h +++ b/esphome/components/display/rect.h @@ -26,7 +26,7 @@ class Rect { void extend(Rect rect); void shrink(Rect rect); - bool inside(Rect rect, bool absolute = true) const; + bool inside(Rect rect) const; bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const; bool equal(Rect rect) const; void info(const std::string &prefix = "rect info:"); From 37a2cb07d1b585209a88ccf55d00af29d35f7c7e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 May 2025 10:28:00 +1000 Subject: [PATCH 042/193] [as3935_i2c] Remove redundant includes (#8677) --- esphome/components/as3935_i2c/as3935_i2c.h | 3 --- tests/components/as3935_i2c/test.esp32-ard.yaml | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h index 1d16397bdf..a2a3d213ef 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.h +++ b/esphome/components/as3935_i2c/as3935_i2c.h @@ -1,10 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/as3935/as3935.h" #include "esphome/components/i2c/i2c.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { namespace as3935_i2c { diff --git a/tests/components/as3935_i2c/test.esp32-ard.yaml b/tests/components/as3935_i2c/test.esp32-ard.yaml index 2c57d412f6..52d5a045cb 100644 --- a/tests/components/as3935_i2c/test.esp32-ard.yaml +++ b/tests/components/as3935_i2c/test.esp32-ard.yaml @@ -3,4 +3,9 @@ substitutions: sda_pin: GPIO17 irq_pin: GPIO15 -<<: !include common.yaml +packages: + as3935: !include common.yaml + +# Trigger issue: https://github.com/esphome/issues/issues/6990 +# Compile with no binary sensor results in error +binary_sensor: !remove From 1294e8ccd5ea662ef3448a9ab5e992b3e78ba79b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 May 2025 10:30:11 +1000 Subject: [PATCH 043/193] [lvgl] Allow padding to be negative (#8671) --- esphome/components/lvgl/lv_validation.py | 11 +++++++- esphome/components/lvgl/schemas.py | 28 +++++++++---------- .../components/lvgl/widgets/buttonmatrix.py | 6 ++-- esphome/components/lvgl/widgets/checkbox.py | 4 +-- tests/components/lvgl/lvgl-package.yaml | 2 ++ 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index a3b7cc8ed3..3755d35d27 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -16,7 +16,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj -from esphome.cpp_types import ESPTime, uint32 +from esphome.cpp_types import ESPTime, int32, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -263,6 +263,15 @@ def pixels_validator(value): pixels = LValidator(pixels_validator, uint32, retmapper=literal) +def padding_validator(value): + if isinstance(value, str) and value.lower().endswith("px"): + value = value[:-2] + return cv.int_(value) + + +padding = LValidator(padding_validator, int32, retmapper=literal) + + def zoom_validator(value): value = cv.float_range(0.1, 10.0)(value) return value diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 051dbe5e0e..d0dde01421 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -156,13 +156,13 @@ STYLE_PROPS = { "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, - "outline_pad": lvalid.pixels, + "outline_pad": lvalid.padding, "outline_width": lvalid.pixels, - "pad_all": lvalid.pixels, - "pad_bottom": lvalid.pixels, - "pad_left": lvalid.pixels, - "pad_right": lvalid.pixels, - "pad_top": lvalid.pixels, + "pad_all": lvalid.padding, + "pad_bottom": lvalid.padding, + "pad_left": lvalid.padding, + "pad_right": lvalid.padding, + "pad_top": lvalid.padding, "shadow_color": lvalid.lv_color, "shadow_ofs_x": lvalid.lv_int, "shadow_ofs_y": lvalid.lv_int, @@ -226,8 +226,8 @@ FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend( { cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + cv.Optional(df.CONF_PAD_ROW): lvalid.padding, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, } ) @@ -370,8 +370,8 @@ LAYOUT_SCHEMA = { cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + cv.Optional(df.CONF_PAD_ROW): lvalid.padding, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, }, df.TYPE_FLEX: { cv.Optional( @@ -380,8 +380,8 @@ LAYOUT_SCHEMA = { cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + cv.Optional(df.CONF_PAD_ROW): lvalid.padding, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, }, }, lower=True, @@ -427,8 +427,8 @@ ALL_STYLES = { **STYLE_PROPS, **GRID_CELL_SCHEMA, **FLEX_OBJ_SCHEMA, - cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + cv.Optional(df.CONF_PAD_ROW): lvalid.padding, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, } diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index 0ba1fe4ae1..aa33be722c 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -19,7 +19,7 @@ from ..defines import ( CONF_SELECTED, ) from ..helpers import lvgl_components_required -from ..lv_validation import key_code, lv_bool, pixels +from ..lv_validation import key_code, lv_bool, padding from ..lvcode import lv, lv_add, lv_expr from ..schemas import automation_schema from ..types import ( @@ -59,8 +59,8 @@ BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema( BUTTONMATRIX_SCHEMA = cv.Schema( { cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool, - cv.Optional(CONF_PAD_ROW): pixels, - cv.Optional(CONF_PAD_COLUMN): pixels, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), cv.Required(CONF_ROWS): cv.ensure_list( cv.Schema( diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py index 75f4142eb1..c344fbfe75 100644 --- a/esphome/components/lvgl/widgets/checkbox.py +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -2,7 +2,7 @@ from esphome.config_validation import Optional from esphome.const import CONF_TEXT from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_PAD_COLUMN -from ..lv_validation import lv_text, pixels +from ..lv_validation import lv_text, padding from ..lvcode import lv from ..schemas import TEXT_SCHEMA from ..types import LvBoolean @@ -19,7 +19,7 @@ class CheckboxType(WidgetType): (CONF_MAIN, CONF_INDICATOR), TEXT_SCHEMA.extend( { - Optional(CONF_PAD_COLUMN): pixels, + Optional(CONF_PAD_COLUMN): padding, } ), ) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index a0b7dd096f..d0e281e583 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -641,6 +641,8 @@ lvgl: knob: radius: 1 width: "4" + pad_left: -5 + pad_top: 5 height: 10% bg_color: 0x000000 width: 100% From d7c615ec43fe7c25b97103db4bf1a7cd279fc93b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 May 2025 10:31:22 +1000 Subject: [PATCH 044/193] [lvgl] Fix image property processing (#8691) --- esphome/components/lvgl/widgets/img.py | 18 ++++++++++-------- tests/components/lvgl/lvgl-package.yaml | 11 +++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index c3e0781489..8ec18e3033 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -10,7 +10,7 @@ from ..defines import ( CONF_ZOOM, LvConstant, ) -from ..lv_validation import angle, lv_bool, lv_image, size, zoom +from ..lv_validation import lv_angle, lv_bool, lv_image, size, zoom from ..lvcode import lv from ..types import lv_img_t from . import Widget, WidgetType @@ -22,7 +22,7 @@ BASE_IMG_SCHEMA = cv.Schema( { cv.Optional(CONF_PIVOT_X): size, cv.Optional(CONF_PIVOT_Y): size, - cv.Optional(CONF_ANGLE): angle, + cv.Optional(CONF_ANGLE): lv_angle, cv.Optional(CONF_ZOOM): zoom, cv.Optional(CONF_OFFSET_X): size, cv.Optional(CONF_OFFSET_Y): size, @@ -66,17 +66,19 @@ class ImgType(WidgetType): if (pivot_x := config.get(CONF_PIVOT_X)) and ( pivot_y := config.get(CONF_PIVOT_Y) ): - lv.img_set_pivot(w.obj, pivot_x, pivot_y) + lv.img_set_pivot( + w.obj, await size.process(pivot_x), await size.process(pivot_y) + ) if (cf_angle := config.get(CONF_ANGLE)) is not None: - lv.img_set_angle(w.obj, cf_angle) + lv.img_set_angle(w.obj, await lv_angle.process(cf_angle)) if (img_zoom := config.get(CONF_ZOOM)) is not None: - lv.img_set_zoom(w.obj, img_zoom) + lv.img_set_zoom(w.obj, await zoom.process(img_zoom)) if (offset := config.get(CONF_OFFSET_X)) is not None: - lv.img_set_offset_x(w.obj, offset) + lv.img_set_offset_x(w.obj, await size.process(offset)) if (offset := config.get(CONF_OFFSET_Y)) is not None: - lv.img_set_offset_y(w.obj, offset) + lv.img_set_offset_y(w.obj, await size.process(offset)) if CONF_ANTIALIAS in config: - lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) + lv.img_set_antialias(w.obj, await lv_bool.process(config[CONF_ANTIALIAS])) if mode := config.get(CONF_MODE): await w.set_property("size_mode", mode) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d0e281e583..6fd0b5e3c4 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -134,6 +134,15 @@ lvgl: id: style_test bg_color: blue bg_opa: !lambda return 0.5; + - lvgl.image.update: + id: lv_image + zoom: !lambda return 512; + angle: !lambda return 100; + pivot_x: !lambda return 20; + pivot_y: !lambda return 20; + offset_x: !lambda return 20; + offset_y: !lambda return 20; + antialias: !lambda return true; - id: simple_msgbox title: Simple @@ -486,6 +495,8 @@ lvgl: align: top_left y: "50" mode: real + zoom: 2.0 + angle: 45 - tileview: id: tileview_id scrollbar_mode: active From cae3c030d271d1dfcb6a79d2ed3a02ea232d29ba Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 12 May 2025 08:52:13 +1200 Subject: [PATCH 045/193] Bump version to 2025.4.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6d1ff157bf..20d64513c9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.4.1" +__version__ = "2025.4.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 04147a7f2767fe8c74a2ad5e89818d0d64e53bb6 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 11 May 2025 14:33:50 -0700 Subject: [PATCH 046/193] [one_wire][dallas_temp] adjust timings and reduce disabled interrupts (#8744) Co-authored-by: Samuel Sieb --- .../components/dallas_temp/dallas_temp.cpp | 41 ++++----- esphome/components/dallas_temp/dallas_temp.h | 1 - .../gpio/one_wire/gpio_one_wire.cpp | 88 ++++++++++--------- .../components/gpio/one_wire/gpio_one_wire.h | 3 +- esphome/components/one_wire/one_wire_bus.cpp | 24 ++--- esphome/components/one_wire/one_wire_bus.h | 22 +++-- 6 files changed, 92 insertions(+), 87 deletions(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index ae567d6a76..46db22d97f 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -56,21 +56,13 @@ void DallasTemperatureSensor::update() { }); } -void IRAM_ATTR DallasTemperatureSensor::read_scratch_pad_int_() { - for (uint8_t &i : this->scratch_pad_) { - i = this->bus_->read8(); - } -} - bool DallasTemperatureSensor::read_scratch_pad_() { - bool success; - { - InterruptLock lock; - success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD); - if (success) - this->read_scratch_pad_int_(); - } - if (!success) { + bool success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD); + if (success) { + for (uint8_t &i : this->scratch_pad_) { + i = this->bus_->read8(); + } + } else { ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); this->status_set_warning("bus reset failed"); } @@ -113,17 +105,14 @@ void DallasTemperatureSensor::setup() { return; this->scratch_pad_[4] = res; - { - InterruptLock lock; - if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) { - this->bus_->write8(this->scratch_pad_[2]); // high alarm temp - this->bus_->write8(this->scratch_pad_[3]); // low alarm temp - this->bus_->write8(this->scratch_pad_[4]); // resolution - } - - // write value to EEPROM - this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD); + if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) { + this->bus_->write8(this->scratch_pad_[2]); // high alarm temp + this->bus_->write8(this->scratch_pad_[3]); // low alarm temp + this->bus_->write8(this->scratch_pad_[4]); // resolution } + + // write value to EEPROM + this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD); } bool DallasTemperatureSensor::check_scratch_pad_() { @@ -138,6 +127,10 @@ bool DallasTemperatureSensor::check_scratch_pad_() { if (!chksum_validity) { ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); this->status_set_warning("scratch pad checksum invalid"); + ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], + this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], + this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], + crc8(this->scratch_pad_, 8)); } return chksum_validity; } diff --git a/esphome/components/dallas_temp/dallas_temp.h b/esphome/components/dallas_temp/dallas_temp.h index 604c9d0cd7..1bd2865095 100644 --- a/esphome/components/dallas_temp/dallas_temp.h +++ b/esphome/components/dallas_temp/dallas_temp.h @@ -23,7 +23,6 @@ class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor, /// Get the number of milliseconds we have to wait for the conversion phase. uint16_t millis_to_wait_for_conversion_() const; bool read_scratch_pad_(); - void read_scratch_pad_int_(); bool check_scratch_pad_(); float get_temp_c_(); }; diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 36eaf2160a..8a56595efb 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -10,8 +10,10 @@ static const char *const TAG = "gpio.one_wire"; void GPIOOneWireBus::setup() { ESP_LOGCONFIG(TAG, "Setting up 1-wire bus..."); this->t_pin_->setup(); - // clear bus with 480µs high, otherwise initial reset in search might fail this->t_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + // clear bus with 480µs high, otherwise initial reset in search might fail + this->pin_.digital_write(true); + this->pin_.pin_mode(gpio::FLAG_OUTPUT); delayMicroseconds(480); this->search(); } @@ -22,40 +24,49 @@ void GPIOOneWireBus::dump_config() { this->dump_devices_(TAG); } -bool HOT IRAM_ATTR GPIOOneWireBus::reset() { +int HOT IRAM_ATTR GPIOOneWireBus::reset_int() { + InterruptLock lock; // See reset here: // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html // Wait for communication to clear (delay G) - pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); uint8_t retries = 125; do { if (--retries == 0) - return false; + return -1; delayMicroseconds(2); - } while (!pin_.digital_read()); + } while (!this->pin_.digital_read()); - bool r; + bool r = false; // Send 480µs LOW TX reset pulse (drive bus low, delay H) - pin_.pin_mode(gpio::FLAG_OUTPUT); - pin_.digital_write(false); + this->pin_.digital_write(false); + this->pin_.pin_mode(gpio::FLAG_OUTPUT); delayMicroseconds(480); // Release the bus, delay I - pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - delayMicroseconds(70); + this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + uint32_t start = micros(); + delayMicroseconds(30); + + while (micros() - start < 300) { + // sample bus, 0=device(s) present, 1=no device present + r = !this->pin_.digital_read(); + if (r) + break; + delayMicroseconds(1); + } - // sample bus, 0=device(s) present, 1=no device present - r = !pin_.digital_read(); // delay J - delayMicroseconds(410); - return r; + delayMicroseconds(start + 480 - micros()); + this->pin_.digital_write(true); + this->pin_.pin_mode(gpio::FLAG_OUTPUT); + return r ? 1 : 0; } void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) { // drive bus low - pin_.pin_mode(gpio::FLAG_OUTPUT); - pin_.digital_write(false); + this->pin_.digital_write(false); // from datasheet: // write 0 low time: t_low0: min=60µs, max=120µs @@ -64,72 +75,62 @@ void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) { // recovery time: t_rec: min=1µs // ds18b20 appears to read the bus after roughly 14µs uint32_t delay0 = bit ? 6 : 60; - uint32_t delay1 = bit ? 59 : 5; + uint32_t delay1 = bit ? 64 : 10; // delay A/C delayMicroseconds(delay0); // release bus - pin_.digital_write(true); + this->pin_.digital_write(true); // delay B/D delayMicroseconds(delay1); } bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() { // drive bus low - pin_.pin_mode(gpio::FLAG_OUTPUT); - pin_.digital_write(false); + this->pin_.digital_write(false); - // note: for reading we'll need very accurate timing, as the - // timing for the digital_read() is tight; according to the datasheet, - // we should read at the end of 16µs starting from the bus low - // typically, the ds18b20 pulls the line high after 11µs for a logical 1 - // and 29µs for a logical 0 - - uint32_t start = micros(); - // datasheet says >1µs - delayMicroseconds(2); + // datasheet says >= 1µs + delayMicroseconds(5); // release bus, delay E - pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - - // measure from start value directly, to get best accurate timing no matter - // how long pin_mode/delayMicroseconds took - uint32_t now = micros(); - if (now - start < 12) - delayMicroseconds(12 - (now - start)); + this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + delayMicroseconds(8); // sample bus to read bit from peer - bool r = pin_.digital_read(); + bool r = this->pin_.digital_read(); - // read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked - now = micros(); - if (now - start < 60) - delayMicroseconds(60 - (now - start)); + // read slot is at least 60µs + delayMicroseconds(50); + this->pin_.digital_write(true); + this->pin_.pin_mode(gpio::FLAG_OUTPUT); return r; } void IRAM_ATTR GPIOOneWireBus::write8(uint8_t val) { + InterruptLock lock; for (uint8_t i = 0; i < 8; i++) { this->write_bit_(bool((1u << i) & val)); } } void IRAM_ATTR GPIOOneWireBus::write64(uint64_t val) { + InterruptLock lock; for (uint8_t i = 0; i < 64; i++) { this->write_bit_(bool((1ULL << i) & val)); } } uint8_t IRAM_ATTR GPIOOneWireBus::read8() { + InterruptLock lock; uint8_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i < 8; i++) ret |= (uint8_t(this->read_bit_()) << i); - } return ret; } uint64_t IRAM_ATTR GPIOOneWireBus::read64() { + InterruptLock lock; uint64_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint64_t(this->read_bit_()) << i); @@ -144,6 +145,7 @@ void GPIOOneWireBus::reset_search() { } uint64_t IRAM_ATTR GPIOOneWireBus::search_int() { + InterruptLock lock; if (this->last_device_flag_) return 0u; diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.h b/esphome/components/gpio/one_wire/gpio_one_wire.h index fe949baec3..8874703971 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.h +++ b/esphome/components/gpio/one_wire/gpio_one_wire.h @@ -18,7 +18,6 @@ class GPIOOneWireBus : public one_wire::OneWireBus, public Component { this->pin_ = pin->to_isr(); } - bool reset() override; void write8(uint8_t val) override; void write64(uint64_t val) override; uint8_t read8() override; @@ -31,10 +30,12 @@ class GPIOOneWireBus : public one_wire::OneWireBus, public Component { bool last_device_flag_{false}; uint64_t address_; + int reset_int() override; void reset_search() override; uint64_t search_int() override; void write_bit_(bool bit); bool read_bit_(); + bool read_bit_(uint32_t *t); }; } // namespace gpio diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index a8d29428d3..c2542177cf 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -17,8 +17,15 @@ const uint8_t ONE_WIRE_ROM_SEARCH = 0xF0; const std::vector &OneWireBus::get_devices() { return this->devices_; } +bool OneWireBus::reset_() { + int res = this->reset_int(); + if (res == -1) + ESP_LOGE(TAG, "1-wire bus is held low"); + return res == 1; +} + bool IRAM_ATTR OneWireBus::select(uint64_t address) { - if (!this->reset()) + if (!this->reset_()) return false; this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); @@ -31,16 +38,13 @@ void OneWireBus::search() { this->reset_search(); uint64_t address; while (true) { - { - InterruptLock lock; - if (!this->reset()) { - // Reset failed or no devices present - return; - } - - this->write8(ONE_WIRE_ROM_SEARCH); - address = this->search_int(); + if (!this->reset_()) { + // Reset failed or no devices present + return; } + + this->write8(ONE_WIRE_ROM_SEARCH); + address = this->search_int(); if (address == 0) break; auto *address8 = reinterpret_cast(&address); diff --git a/esphome/components/one_wire/one_wire_bus.h b/esphome/components/one_wire/one_wire_bus.h index 6818b17499..c88532046f 100644 --- a/esphome/components/one_wire/one_wire_bus.h +++ b/esphome/components/one_wire/one_wire_bus.h @@ -9,14 +9,6 @@ namespace one_wire { class OneWireBus { public: - /** Reset the bus, should be done before all write operations. - * - * Takes approximately 1ms. - * - * @return Whether the operation was successful. - */ - virtual bool reset() = 0; - /// Write a word to the bus. LSB first. virtual void write8(uint8_t val) = 0; @@ -50,6 +42,20 @@ class OneWireBus { /// log the found devices void dump_devices_(const char *tag); + /** Reset the bus, should be done before all write operations. + * + * Takes approximately 1ms. + * + * @return Whether the operation was successful. + */ + bool reset_(); + + /** + * Bus Reset + * @return -1: signal fail, 0: no device detected, 1: device detected + */ + virtual int reset_int() = 0; + /// Reset the device search. virtual void reset_search() = 0; From 3e2359ddff65da27e3eebfa01a83232f53dc8122 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 10:05:24 +1200 Subject: [PATCH 047/193] Bump aioesphomeapi from 30.1.0 to 30.2.0 (#8734) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d95edd1e91..f91d86f75b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250415.0 -aioesphomeapi==30.1.0 +aioesphomeapi==30.2.0 zeroconf==0.146.5 puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import From e2c8a5b638d856de24e014dc070ade2242375f7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 10:19:10 +1200 Subject: [PATCH 048/193] Bump ruff from 0.11.8 to 0.11.9 (#8735) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a63d5fc9c9..c3d5b9c783 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.0 + rev: v0.11.9 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index a920ba0edc..6dd8d883ba 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.2.0 # also change in .pre-commit-config.yaml when updating -ruff==0.11.8 # also change in .pre-commit-config.yaml when updating +ruff==0.11.9 # also change in .pre-commit-config.yaml when updating pyupgrade==3.19.1 # also change in .pre-commit-config.yaml when updating pre-commit From 401c090edd98e6c59c3caaab18ac7126174bab3d Mon Sep 17 00:00:00 2001 From: Mateusz Bronk <2566147+mbronk@users.noreply.github.com> Date: Mon, 12 May 2025 00:28:46 +0200 Subject: [PATCH 049/193] MQTT: fan direction control added (#8022) Co-authored-by: Mateusz Bronk Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/fan/__init__.py | 16 ++++++++++++ esphome/components/mqtt/mqtt_const.h | 4 +++ esphome/components/mqtt/mqtt_fan.cpp | 39 ++++++++++++++++++++++++++++ esphome/components/mqtt/mqtt_fan.h | 2 ++ esphome/const.py | 2 ++ tests/components/mqtt/common.yaml | 2 ++ 6 files changed, 65 insertions(+) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 4e0e52cd65..9c9cb6327b 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server import esphome.config_validation as cv from esphome.const import ( CONF_DIRECTION, + CONF_DIRECTION_COMMAND_TOPIC, + CONF_DIRECTION_STATE_TOPIC, CONF_ID, CONF_MQTT_ID, CONF_OFF_SPEED_CYCLE, @@ -90,6 +92,12 @@ FAN_SCHEMA = ( RESTORE_MODES, upper=True, space="_" ), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), + cv.Optional(CONF_DIRECTION_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_DIRECTION_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), cv.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -193,6 +201,14 @@ async def setup_fan_core_(var, config): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) + if ( + direction_state_topic := config.get(CONF_DIRECTION_STATE_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_direction_state_topic(direction_state_topic)) + if ( + direction_command_topic := config.get(CONF_DIRECTION_COMMAND_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_direction_command_topic(direction_command_topic)) if ( oscillation_state_topic := config.get(CONF_OSCILLATION_STATE_TOPIC) ) is not None: diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 445457a27f..3ddd8fc5cc 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -64,6 +64,8 @@ constexpr const char *const MQTT_DEVICE_NAME = "name"; constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw"; +constexpr const char *const MQTT_DIRECTION_COMMAND_TOPIC = "dir_cmd_t"; +constexpr const char *const MQTT_DIRECTION_STATE_TOPIC = "dir_stat_t"; constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t"; @@ -328,6 +330,8 @@ constexpr const char *const MQTT_DEVICE_NAME = "name"; constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw_version"; +constexpr const char *const MQTT_DIRECTION_COMMAND_TOPIC = "direction_command_topic"; +constexpr const char *const MQTT_DIRECTION_STATE_TOPIC = "direction_state_topic"; constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic"; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 32892199fe..9e5ea54bee 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -43,6 +43,32 @@ void MQTTFanComponent::setup() { } }); + if (this->state_->get_traits().supports_direction()) { + this->subscribe(this->get_direction_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto val = parse_on_off(payload.c_str(), "forward", "reverse"); + switch (val) { + case PARSE_ON: + ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str()); + this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform(); + break; + case PARSE_OFF: + ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str()); + this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform(); + break; + case PARSE_TOGGLE: + this->state_->make_call() + .set_direction(this->state_->direction == fan::FanDirection::FORWARD ? fan::FanDirection::REVERSE + : fan::FanDirection::FORWARD) + .perform(); + break; + case PARSE_NONE: + ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str()); + this->status_momentary_warning("direction", 5000); + break; + } + }); + } + if (this->state_->get_traits().supports_oscillation()) { this->subscribe(this->get_oscillation_command_topic(), [this](const std::string &topic, const std::string &payload) { @@ -94,6 +120,10 @@ void MQTTFanComponent::setup() { void MQTTFanComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Fan '%s': ", this->state_->get_name().c_str()); LOG_MQTT_COMPONENT(true, true); + if (this->state_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction State Topic: '%s'", this->get_direction_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Direction Command Topic: '%s'", this->get_direction_command_topic().c_str()); + } if (this->state_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation State Topic: '%s'", this->get_oscillation_state_topic().c_str()); ESP_LOGCONFIG(TAG, " Oscillation Command Topic: '%s'", this->get_oscillation_command_topic().c_str()); @@ -107,6 +137,10 @@ void MQTTFanComponent::dump_config() { bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + if (this->state_->get_traits().supports_direction()) { + root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); + root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); + } if (this->state_->get_traits().supports_oscillation()) { root[MQTT_OSCILLATION_COMMAND_TOPIC] = this->get_oscillation_command_topic(); root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic(); @@ -122,6 +156,11 @@ bool MQTTFanComponent::publish_state() { ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s); this->publish(this->get_state_topic_(), state_s); bool failed = false; + if (this->state_->get_traits().supports_direction()) { + bool success = this->publish(this->get_direction_state_topic(), + this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse"); + failed = failed || !success; + } if (this->state_->get_traits().supports_oscillation()) { bool success = this->publish(this->get_oscillation_state_topic(), this->state_->oscillating ? "oscillate_on" : "oscillate_off"); diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 12286b9f01..fdcec0782d 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -15,6 +15,8 @@ class MQTTFanComponent : public mqtt::MQTTComponent { public: explicit MQTTFanComponent(fan::Fan *state); + MQTT_COMPONENT_CUSTOM_TOPIC(direction, command) + MQTT_COMPONENT_CUSTOM_TOPIC(direction, state) MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, command) MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, state) MQTT_COMPONENT_CUSTOM_TOPIC(speed_level, command) diff --git a/esphome/const.py b/esphome/const.py index 3b84055789..d656b15519 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -221,7 +221,9 @@ CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" +CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic" CONF_DIRECTION_OUTPUT = "direction_output" +CONF_DIRECTION_STATE_TOPIC = "direction_state_topic" CONF_DISABLE_CRC = "disable_crc" CONF_DISABLED = "disabled" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index a4bdf58809..1ab8872fdb 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -293,6 +293,8 @@ fan: - platform: template name: Template Fan state_topic: some/topic/fan + direction_state_topic: some/topic/direction/state + direction_command_topic: some/topic/direction/command qos: 2 on_state: - logger.log: on_state From 8324b3244c17870bdc87d965ca66739d3fb93410 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 12 May 2025 18:31:36 +1200 Subject: [PATCH 050/193] [config] Add entity schema consts with deprecation log (#8747) --- esphome/components/lock/__init__.py | 5 +++++ esphome/components/text/__init__.py | 5 +++++ esphome/components/update/__init__.py | 5 +++++ esphome/components/valve/__init__.py | 5 +++++ esphome/config_validation.py | 17 +++++++++++++++++ 5 files changed, 37 insertions(+) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 8bf7af3de2..a96290dca6 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -88,6 +88,11 @@ def lock_schema( return _LOCK_SCHEMA.extend(schema) +# Remove before 2025.11.0 +LOCK_SCHEMA = lock_schema() +LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) + + async def _setup_lock_core(var, config): await setup_entity(var, config) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 39626c2c5c..1cc9283e45 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -83,6 +83,11 @@ def text_schema( return _TEXT_SCHEMA.extend(schema) +# Remove before 2025.11.0 +TEXT_SCHEMA = text_schema() +TEXT_SCHEMA.add_extra(cv.deprecated_schema_constant("text")) + + async def setup_text_core_( var, config, diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index a607aefea0..c2654520fd 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -81,6 +81,11 @@ def update_schema( return _UPDATE_SCHEMA.extend(schema) +# Remove before 2025.11.0 +UPDATE_SCHEMA = update_schema() +UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) + + async def setup_update_core_(var, config): await setup_entity(var, config) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 76ad76e8d0..f3c0353777 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -126,6 +126,11 @@ def valve_schema( return _VALVE_SCHEMA.extend(schema) +# Remove before 2025.11.0 +VALVE_SCHEMA = valve_schema() +VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) + + async def _setup_valve_core(var, config): await setup_entity(var, config) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 993fcfac5b..88a805591d 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2072,3 +2072,20 @@ def rename_key(old_key, new_key): return config return validator + + +# Remove before 2025.11.0 +def deprecated_schema_constant(entity_type: str): + def validator(config): + _LOGGER.warning( + "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " + "Please use `%s.%s_schema(...)` instead. " + "If you are seeing this, report an issue to the external_component author and ask them to update it.", + entity_type, + entity_type.upper(), + entity_type, + entity_type, + ) + return config + + return validator From dded81d6226bfee279a4ba2026f929222ccdc39a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 May 2025 14:03:34 -0500 Subject: [PATCH 051/193] Fix ESP32 API Disconnects Caused by Concurrent Logger Writes (#8736) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/logger/__init__.py | 21 ++ esphome/components/logger/logger.cpp | 221 ++++++++++-------- esphome/components/logger/logger.h | 197 +++++++++++++--- esphome/components/logger/task_log_buffer.cpp | 138 +++++++++++ esphome/components/logger/task_log_buffer.h | 69 ++++++ .../test-custom_buffer_size.esp32-idf.yaml | 5 + .../test-disable_log_buffer.esp32-idf.yaml | 5 + .../test-max_buffer_size.esp32-idf.yaml | 5 + .../test-min_buffer_size.esp32-idf.yaml | 5 + 9 files changed, 533 insertions(+), 133 deletions(-) create mode 100644 esphome/components/logger/task_log_buffer.cpp create mode 100644 esphome/components/logger/task_log_buffer.h create mode 100644 tests/components/logger/test-custom_buffer_size.esp32-idf.yaml create mode 100644 tests/components/logger/test-disable_log_buffer.esp32-idf.yaml create mode 100644 tests/components/logger/test-max_buffer_size.esp32-idf.yaml create mode 100644 tests/components/logger/test-min_buffer_size.esp32-idf.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 113f306327..01e75a424d 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -79,6 +79,7 @@ DEFAULT = "DEFAULT" CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" +CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], @@ -180,6 +181,20 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, + cv.SplitDefault( + CONF_TASK_LOG_BUFFER_SIZE, + esp32=768, # Default: 768 bytes (~5-6 messages with 70-byte text plus thread names) + ): cv.All( + cv.only_on_esp32, + cv.validate_bytes, + cv.Any( + cv.int_(0), # Disabled + cv.int_range( + min=640, # Min: ~4-5 messages with 70-byte text plus thread names + max=32768, # Max: Depends on message sizes, typically ~300 messages with default size + ), + ), + ), cv.SplitDefault( CONF_HARDWARE_UART, esp8266=UART0, @@ -238,6 +253,12 @@ async def to_code(config): baud_rate, config[CONF_TX_BUFFER_SIZE], ) + if CORE.is_esp32: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + cg.add(log.init_log_buffer(task_log_buffer_size)) + cg.add(log.set_log_level(initial_level)) if CONF_HARDWARE_UART in config: cg.add( diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 57f0ba9f9a..03e42cdd48 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -1,5 +1,8 @@ #include "logger.h" #include +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +#include // For unique_ptr +#endif #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -10,127 +13,118 @@ namespace logger { static const char *const TAG = "logger"; -static const char *const LOG_LEVEL_COLORS[] = { - "", // NONE - ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE -}; -static const char *const LOG_LEVEL_LETTERS[] = { - "", // NONE - "E", // ERROR - "W", // WARNING - "I", // INFO - "C", // CONFIG - "D", // DEBUG - "V", // VERBOSE - "VV", // VERY_VERBOSE -}; +#ifdef USE_ESP32 +// Implementation for ESP32 (multi-core with atomic support) +// Main thread: synchronous logging with direct buffer access +// Other threads: console output with stack buffer, callbacks via async buffer +void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT + if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed)) + return; + recursion_guard_.store(true, std::memory_order_relaxed); -void Logger::write_header_(int level, const char *tag, int line) { - if (level < 0) - level = 0; - if (level > 7) - level = 7; - - const char *color = LOG_LEVEL_COLORS[level]; - const char *letter = LOG_LEVEL_LETTERS[level]; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); -#else - void *current_task = nullptr; -#endif - if (current_task == main_task_) { - this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line); - } else { - const char *thread_name = ""; // NOLINT(clang-analyzer-deadcode.DeadStores) -#if defined(USE_ESP32) - thread_name = pcTaskGetName(current_task); -#elif defined(USE_LIBRETINY) - thread_name = pcTaskGetTaskName(current_task); -#endif - this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, - ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); - } -} + // For main task: call log_message_to_buffer_and_send_ which does console and callback logging + if (current_task == main_task_) { + this->log_message_to_buffer_and_send_(level, tag, line, format, args); + recursion_guard_.store(false, std::memory_order_release); + return; + } + + // For non-main tasks: use stack-allocated buffer only for console output + if (this->baud_rate_ > 0) { // If logging is enabled, write to console + // Maximum size for console log messages (includes null terminator) + static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; + char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety + int buffer_at = 0; // Initialize buffer position + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, + MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer); + } + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered + if (this->log_callback_.size() > 0) { + // This will be processed in the main loop + this->log_buffer_->send_message_thread_safe(static_cast(level), tag, static_cast(line), + current_task, format, args); + } +#endif // USE_ESPHOME_TASK_LOG_BUFFER + + recursion_guard_.store(false, std::memory_order_release); +} +#endif // USE_ESP32 + +#ifndef USE_ESP32 +// Implementation for platforms that do not support atomic operations +// or have to consider logging in other tasks void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || recursion_guard_) return; recursion_guard_ = true; - this->reset_buffer_(); - this->write_header_(level, tag, line); - this->vprintf_to_buffer_(format, args); - this->write_footer_(); - this->log_message_(level, tag); + + // Format and send to both console and callbacks + this->log_message_to_buffer_and_send_(level, tag, line, format, args); + recursion_guard_ = false; } +#endif // !USE_ESP32 + #ifdef USE_STORE_LOG_STR_IN_FLASH +// Implementation for ESP8266 with flash string support void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || recursion_guard_) return; recursion_guard_ = true; - this->reset_buffer_(); - // copy format string + this->tx_buffer_at_ = 0; + + // Copy format string from progmem auto *format_pgm_p = reinterpret_cast(format); - size_t len = 0; char ch = '.'; - while (!this->is_buffer_full_() && ch != '\0') { + while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') { this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++); } - // Buffer full form copying format - if (this->is_buffer_full_()) + + // Buffer full from copying format + if (this->tx_buffer_at_ >= this->tx_buffer_size_) return; - // length of format string, includes null terminator - uint32_t offset = this->tx_buffer_at_; + // Save the offset before calling format_log_to_buffer_with_terminator_ + // since it will increment tx_buffer_at_ to the end of the formatted string + uint32_t msg_start = this->tx_buffer_at_; + this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, + &this->tx_buffer_at_, this->tx_buffer_size_); + + // No write console and callback starting at the msg_start + if (this->baud_rate_ > 0) { + this->write_msg_(this->tx_buffer_ + msg_start); + } + this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); - // now apply vsnprintf - this->write_header_(level, tag, line); - this->vprintf_to_buffer_(this->tx_buffer_, args); - this->write_footer_(); - this->log_message_(level, tag, offset); recursion_guard_ = false; } -#endif +#endif // USE_STORE_LOG_STR_IN_FLASH -int HOT Logger::level_for(const char *tag) { - if (this->log_levels_.count(tag) != 0) - return this->log_levels_[tag]; +inline int Logger::level_for(const char *tag) { + auto it = this->log_levels_.find(tag); + if (it != this->log_levels_.end()) + return it->second; return this->current_level_; } -void HOT Logger::log_message_(int level, const char *tag, int offset) { - // remove trailing newline - if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') { - this->tx_buffer_at_--; - } - // make sure null terminator is present - this->set_null_terminator_(); - - const char *msg = this->tx_buffer_ + offset; - - if (this->baud_rate_ > 0) { - this->write_msg_(msg); - } - +void HOT Logger::call_log_callbacks_(int level, const char *tag, const char *msg) { #ifdef USE_ESP32 - // Suppress network-logging if memory constrained, but still log to serial - // ports. In some configurations (eg BLE enabled) there may be some transient + // Suppress network-logging if memory constrained + // In some configurations (eg BLE enabled) there may be some transient // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping // here usually allows the stack to recover instead. // See issue #1234 for analysis. if (xPortGetFreeHeapSize() < 2048) return; #endif - this->log_callback_.call(level, tag, msg); } @@ -141,21 +135,50 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate this->main_task_ = xTaskGetCurrentTaskHandle(); #endif } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +void Logger::init_log_buffer(size_t total_buffer_size) { + this->log_buffer_ = esphome::make_unique(total_buffer_size); +} +#endif -#ifdef USE_LOGGER_USB_CDC +#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) void Logger::loop() { -#ifdef USE_ARDUINO - if (this->uart_ != UART_SELECTION_USB_CDC) { - return; +#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) + if (this->uart_ == UART_SELECTION_USB_CDC) { + static bool opened = false; + if (opened == Serial) { + return; + } + if (false == opened) { + App.schedule_dump_config(); + } + opened = !opened; } - static bool opened = false; - if (opened == Serial) { - return; +#endif + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + // Process any buffered messages when available + if (this->log_buffer_->has_messages()) { + logger::TaskLogBuffer::LogMessage *message; + const char *text; + void *received_token; + + // Process messages from the buffer + while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { + this->tx_buffer_at_ = 0; + // Use the thread name that was stored when the message was created + // This avoids potential crashes if the task no longer exists + const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + this->write_header_to_buffer_(message->level, message->tag, message->line, thread_name, this->tx_buffer_, + &this->tx_buffer_at_, this->tx_buffer_size_); + this->write_body_to_buffer_(text, message->text_length, this->tx_buffer_, &this->tx_buffer_at_, + this->tx_buffer_size_); + this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); + this->tx_buffer_[this->tx_buffer_at_] = '\0'; + this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); + this->log_buffer_->release_message_main_loop(received_token); + } } - if (false == opened) { - App.schedule_dump_config(); - } - opened = !opened; #endif } #endif @@ -171,7 +194,7 @@ void Logger::add_on_log_callback(std::functionlog_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } -const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); @@ -181,12 +204,16 @@ void Logger::dump_config() { ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_); ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_()); #endif +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + if (this->log_buffer_) { + ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u", this->log_buffer_->size()); + } +#endif for (auto &it : this->log_levels_) { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]); } } -void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); } void Logger::set_log_level(int level) { if (level > ESPHOME_LOG_LEVEL) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c4c873e020..8619cc0992 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,12 +2,19 @@ #include #include +#ifdef USE_ESP32 +#include +#endif #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +#include "task_log_buffer.h" +#endif + #ifdef USE_ARDUINO #if defined(USE_ESP8266) || defined(USE_ESP32) #include @@ -26,6 +33,29 @@ namespace esphome { namespace logger { +// Color and letter constants for log levels +static const char *const LOG_LEVEL_COLORS[] = { + "", // NONE + ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE +}; + +static const char *const LOG_LEVEL_LETTERS[] = { + "", // NONE + "E", // ERROR + "W", // WARNING + "I", // INFO + "C", // CONFIG + "D", // DEBUG + "V", // VERBOSE + "VV", // VERY_VERBOSE +}; + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) /** Enum for logging UART selection * @@ -57,7 +87,10 @@ enum UARTSelection { class Logger : public Component { public: explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); -#ifdef USE_LOGGER_USB_CDC +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + void init_log_buffer(size_t total_buffer_size); +#endif +#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -87,7 +120,7 @@ class Logger : public Component { void pre_setup(); void dump_config() override; - int level_for(const char *tag); + inline int level_for(const char *tag); /// Register a callback that will be called for every log message sent void add_on_log_callback(std::function &&callback); @@ -103,46 +136,66 @@ class Logger : public Component { #endif protected: - void write_header_(int level, const char *tag, int line); - void write_footer_(); - void log_message_(int level, const char *tag, int offset = 0); + void call_log_callbacks_(int level, const char *tag, const char *msg); void write_msg_(const char *msg); - inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; } - inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; } - inline void reset_buffer_() { this->tx_buffer_at_ = 0; } - inline void set_null_terminator_() { - // does not increment buffer_at - this->tx_buffer_[this->tx_buffer_at_] = '\0'; - } - inline void write_to_buffer_(char value) { - if (!this->is_buffer_full_()) - this->tx_buffer_[this->tx_buffer_at_++] = value; - } - inline void write_to_buffer_(const char *value, int length) { - for (int i = 0; i < length && !this->is_buffer_full_(); i++) { - this->tx_buffer_[this->tx_buffer_at_++] = value[i]; + // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator + // It's the caller's responsibility to initialize buffer_at (typically to 0) + inline void HOT format_log_to_buffer_with_terminator_(int level, const char *tag, int line, const char *format, + va_list args, char *buffer, int *buffer_at, int buffer_size) { +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); +#else + this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); +#endif + this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, args); + this->write_footer_to_buffer_(buffer, buffer_at, buffer_size); + + // Always ensure the buffer has a null terminator, even if we need to + // overwrite the last character of the actual content + if (*buffer_at >= buffer_size) { + buffer[buffer_size - 1] = '\0'; // Truncate and ensure null termination + } else { + buffer[*buffer_at] = '\0'; // Normal case, append null terminator } } - inline void vprintf_to_buffer_(const char *format, va_list args) { - if (this->is_buffer_full_()) - return; - int remaining = this->buffer_remaining_capacity_(); - int ret = vsnprintf(this->tx_buffer_ + this->tx_buffer_at_, remaining, format, args); - if (ret < 0) { - // Encoding error, do not increment buffer_at + + // Helper to format and send a log message to both console and callbacks + inline void HOT log_message_to_buffer_and_send_(int level, const char *tag, int line, const char *format, + va_list args) { + // Format to tx_buffer and prepare for output + this->tx_buffer_at_ = 0; // Initialize buffer position + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, + this->tx_buffer_size_); + + if (this->baud_rate_ > 0) { + this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console + } + this->call_log_callbacks_(level, tag, this->tx_buffer_); + } + + // Write the body of the log message to the buffer + inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, int *buffer_at, int buffer_size) { + // Calculate available space + const int available = buffer_size - *buffer_at; + if (available <= 0) return; + + // Determine copy length (minimum of remaining capacity and string length) + const size_t copy_len = (length < static_cast(available)) ? length : available; + + // Copy the data + if (copy_len > 0) { + memcpy(buffer + *buffer_at, value, copy_len); + *buffer_at += copy_len; } - if (ret >= remaining) { - // output was too long, truncated - ret = remaining; - } - this->tx_buffer_at_ += ret; } - inline void printf_to_buffer_(const char *format, ...) { + + // Format string to explicit buffer with varargs + inline void printf_to_buffer_(const char *format, char *buffer, int *buffer_at, int buffer_size, ...) { va_list arg; - va_start(arg, format); - this->vprintf_to_buffer_(format, arg); + va_start(arg, buffer_size); + this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); va_end(arg); } @@ -169,10 +222,82 @@ class Logger : public Component { std::map log_levels_{}; CallbackManager log_callback_{}; int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; - /// Prevents recursive log calls, if true a log message is already being processed. - bool recursion_guard_ = false; +#ifdef USE_ESP32 + std::atomic recursion_guard_{false}; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#endif +#else + bool recursion_guard_{false}; +#endif void *main_task_ = nullptr; CallbackManager level_callback_{}; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + const char *HOT get_thread_name_() { + TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); + if (current_task == main_task_) { + return nullptr; // Main task + } else { +#if defined(USE_ESP32) + return pcTaskGetName(current_task); +#elif defined(USE_LIBRETINY) + return pcTaskGetTaskName(current_task); +#endif + } + } +#endif + + inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, + int *buffer_at, int buffer_size) { + // Format header + if (level < 0) + level = 0; + if (level > 7) + level = 7; + + const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; + const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + if (thread_name != nullptr) { + // Non-main task with thread name + this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", buffer, buffer_at, buffer_size, color, letter, tag, line, + ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); + return; + } +#endif + // Main task or non ESP32/LibreTiny platform + this->printf_to_buffer_("%s[%s][%s:%03u]: ", buffer, buffer_at, buffer_size, color, letter, tag, line); + } + + inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, + va_list args) { + // Get remaining capacity in the buffer + const int remaining = buffer_size - *buffer_at; + if (remaining <= 0) + return; + + const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); + + if (ret < 0) { + return; // Encoding error, do not increment buffer_at + } + + // Update buffer_at with the formatted length (handle truncation) + int formatted_len = (ret >= remaining) ? remaining : ret; + *buffer_at += formatted_len; + + // Remove all trailing newlines right after formatting + while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') { + (*buffer_at)--; + } + } + + inline void HOT write_footer_to_buffer_(char *buffer, int *buffer_at, int buffer_size) { + static const int RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); + this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); + } }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/task_log_buffer.cpp b/esphome/components/logger/task_log_buffer.cpp new file mode 100644 index 0000000000..24d9284f1a --- /dev/null +++ b/esphome/components/logger/task_log_buffer.cpp @@ -0,0 +1,138 @@ + +#include "task_log_buffer.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +namespace esphome { +namespace logger { + +TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { + // Store the buffer size + this->size_ = total_buffer_size; + // Allocate memory for the ring buffer using ESPHome's RAM allocator + RAMAllocator allocator; + this->storage_ = allocator.allocate(this->size_); + // Create a static ring buffer with RINGBUF_TYPE_NOSPLIT for message integrity + this->ring_buffer_ = xRingbufferCreateStatic(this->size_, RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); +} + +TaskLogBuffer::~TaskLogBuffer() { + if (this->ring_buffer_ != nullptr) { + // Delete the ring buffer + vRingbufferDelete(this->ring_buffer_); + this->ring_buffer_ = nullptr; + + // Free the allocated memory + RAMAllocator allocator; + allocator.deallocate(this->storage_, this->size_); + this->storage_ = nullptr; + } +} + +bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) { + if (message == nullptr || text == nullptr || received_token == nullptr) { + return false; + } + + size_t item_size = 0; + void *received_item = xRingbufferReceive(ring_buffer_, &item_size, 0); + if (received_item == nullptr) { + return false; + } + + LogMessage *msg = static_cast(received_item); + *message = msg; + *text = msg->text_data(); + *received_token = received_item; + + return true; +} + +void TaskLogBuffer::release_message_main_loop(void *token) { + if (token == nullptr) { + return; + } + vRingbufferReturnItem(ring_buffer_, token); + // Update counter to mark all messages as processed + last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); +} + +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, + const char *format, va_list args) { + // First, calculate the exact length needed using a null buffer (no actual writing) + va_list args_copy; + va_copy(args_copy, args); + int ret = vsnprintf(nullptr, 0, format, args_copy); + va_end(args_copy); + + if (ret <= 0) { + return false; // Formatting error or empty message + } + + // Calculate actual text length (capped to maximum size) + static constexpr size_t MAX_TEXT_SIZE = 255; + size_t text_length = (static_cast(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret; + + // Calculate total size needed (header + text length + null terminator) + size_t total_size = sizeof(LogMessage) + text_length + 1; + + // Acquire memory directly from the ring buffer + void *acquired_memory = nullptr; + BaseType_t result = xRingbufferSendAcquire(ring_buffer_, &acquired_memory, total_size, 0); + + if (result != pdTRUE || acquired_memory == nullptr) { + return false; // Failed to acquire memory + } + + // Set up the message header in the acquired memory + LogMessage *msg = static_cast(acquired_memory); + msg->level = level; + msg->tag = tag; + msg->line = line; + + // Store the thread name now instead of waiting until main loop processing + // This avoids crashes if the task completes or is deleted between when this message + // is enqueued and when it's processed by the main loop + const char *thread_name = pcTaskGetName(task_handle); + if (thread_name != nullptr) { + strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); + msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination + } else { + msg->thread_name[0] = '\0'; // Empty string if no thread name + } + + // Format the message text directly into the acquired memory + // We add 1 to text_length to ensure space for null terminator during formatting + char *text_area = msg->text_data(); + ret = vsnprintf(text_area, text_length + 1, format, args); + + // Handle unexpected formatting error + if (ret <= 0) { + vRingbufferReturnItem(ring_buffer_, acquired_memory); + return false; + } + + // Remove trailing newlines + while (text_length > 0 && text_area[text_length - 1] == '\n') { + text_length--; + } + + msg->text_length = text_length; + // Complete the send operation with the acquired memory + result = xRingbufferSendComplete(ring_buffer_, acquired_memory); + + if (result != pdTRUE) { + return false; // Failed to complete the message send + } + + // Message sent successfully, increment the counter + message_counter_.fetch_add(1, std::memory_order_relaxed); + return true; +} + +} // namespace logger +} // namespace esphome + +#endif // USE_ESPHOME_TASK_LOG_BUFFER diff --git a/esphome/components/logger/task_log_buffer.h b/esphome/components/logger/task_log_buffer.h new file mode 100644 index 0000000000..1618a5a121 --- /dev/null +++ b/esphome/components/logger/task_log_buffer.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace logger { + +class TaskLogBuffer { + public: + // Structure for a log message header (text data follows immediately after) + struct LogMessage { + const char *tag; // We store the pointer, assuming tags are static + char thread_name[16]; // Store thread name directly (only used for non-main threads) + uint16_t text_length; // Length of the message text (up to ~64KB) + uint16_t line; // Source code line number + uint8_t level; // Log level (0-7) + + // Methods for accessing message contents + inline char *text_data() { return reinterpret_cast(this) + sizeof(LogMessage); } + + inline const char *text_data() const { return reinterpret_cast(this) + sizeof(LogMessage); } + }; + + // Constructor that takes a total buffer size + explicit TaskLogBuffer(size_t total_buffer_size); + ~TaskLogBuffer(); + + // NOT thread-safe - borrow a message from the ring buffer, only call from main loop + bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token); + + // NOT thread-safe - release a message buffer and update the counter, only call from main loop + void release_message_main_loop(void *token); + + // Thread-safe - send a message to the ring buffer from any thread + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, + const char *format, va_list args); + + // Check if there are messages ready to be processed using an atomic counter for performance + inline bool HOT has_messages() const { + return message_counter_.load(std::memory_order_relaxed) != last_processed_counter_; + } + + // Get the total buffer size in bytes + inline size_t size() const { return size_; } + + private: + RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle + StaticRingbuffer_t structure_; // Static structure for the ring buffer + uint8_t *storage_{nullptr}; // Pointer to allocated memory + size_t size_{0}; // Size of allocated memory + + // Atomic counter for message tracking (only differences matter) + std::atomic message_counter_{0}; // Incremented when messages are committed + mutable uint16_t last_processed_counter_{0}; // Tracks last processed message +}; + +} // namespace logger +} // namespace esphome + +#endif // USE_ESPHOME_TASK_LOG_BUFFER diff --git a/tests/components/logger/test-custom_buffer_size.esp32-idf.yaml b/tests/components/logger/test-custom_buffer_size.esp32-idf.yaml new file mode 100644 index 0000000000..9a396ca023 --- /dev/null +++ b/tests/components/logger/test-custom_buffer_size.esp32-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common-default_uart.yaml + +logger: + id: logger_id + task_log_buffer_size: 1024B # Set a custom buffer size diff --git a/tests/components/logger/test-disable_log_buffer.esp32-idf.yaml b/tests/components/logger/test-disable_log_buffer.esp32-idf.yaml new file mode 100644 index 0000000000..4260f178f9 --- /dev/null +++ b/tests/components/logger/test-disable_log_buffer.esp32-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common-default_uart.yaml + +logger: + id: logger_id + task_log_buffer_size: 0 diff --git a/tests/components/logger/test-max_buffer_size.esp32-idf.yaml b/tests/components/logger/test-max_buffer_size.esp32-idf.yaml new file mode 100644 index 0000000000..f6c3eae677 --- /dev/null +++ b/tests/components/logger/test-max_buffer_size.esp32-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common-default_uart.yaml + +logger: + id: logger_id + task_log_buffer_size: 32768B # Maximum buffer size diff --git a/tests/components/logger/test-min_buffer_size.esp32-idf.yaml b/tests/components/logger/test-min_buffer_size.esp32-idf.yaml new file mode 100644 index 0000000000..715b0580ed --- /dev/null +++ b/tests/components/logger/test-min_buffer_size.esp32-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common-default_uart.yaml + +logger: + id: logger_id + task_log_buffer_size: 640B # Minimum buffer size with thread names From 11dcaf7383beb19bc8609f1589f0511966e1c270 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 12 May 2025 16:27:07 -0300 Subject: [PATCH 052/193] [vscode] provide version to editor (#8752) --- esphome/vscode.py | 15 +++++++++++++++ tests/unit_tests/test_vscode.py | 8 ++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/vscode.py b/esphome/vscode.py index fb62b60eac..d8cfe91938 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -7,6 +7,7 @@ from typing import Any from esphome.config import Config, _format_vol_invalid, validate_config import esphome.config_validation as cv +from esphome.const import __version__ as ESPHOME_VERSION from esphome.core import CORE, DocumentRange from esphome.yaml_util import parse_yaml @@ -97,7 +98,21 @@ def _ace_loader(fname: str) -> dict[str, Any]: return parse_yaml(fname, raw_yaml_stream) +def _print_version(): + """Print ESPHome version.""" + print( + json.dumps( + { + "type": "version", + "value": ESPHOME_VERSION, + } + ) + ) + + def read_config(args): + _print_version() + while True: CORE.reset() data = json.loads(input()) diff --git a/tests/unit_tests/test_vscode.py b/tests/unit_tests/test_vscode.py index f5ebd63f60..6e0bde23b2 100644 --- a/tests/unit_tests/test_vscode.py +++ b/tests/unit_tests/test_vscode.py @@ -18,8 +18,12 @@ def _run_repl_test(input_data): vscode.read_config(args) # Capture printed output - full_output = "".join(call[0][0] for call in mock_stdout.write.call_args_list) - return full_output.strip().split("\n") + full_output = "".join( + call[0][0] for call in mock_stdout.write.call_args_list + ).strip() + splitted_output = full_output.split("\n") + remove_version = splitted_output[1:] # remove first entry with version info + return remove_version def _validate(file_path: str): From 71e88fe9b2e42d7c251fbe53a433ad8bf8394f84 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 12 May 2025 14:30:58 -0500 Subject: [PATCH 053/193] [i2s_audio] Correct a microphone with a DC offset signal (#8751) --- .../i2s_audio/microphone/__init__.py | 8 +- .../microphone/i2s_audio_microphone.cpp | 85 +++++++++++++------ .../microphone/i2s_audio_microphone.h | 16 ++++ tests/components/microphone/common.yaml | 1 + 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 1fb4e9df99..7bbb94f6e3 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -30,6 +30,7 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" +CONF_CORRECT_DC_OFFSET = "correct_dc_offset" CONF_PDM = "pdm" I2SAudioMicrophone = i2s_audio_ns.class_( @@ -88,10 +89,13 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( default_sample_rate=16000, default_channel=CONF_RIGHT, default_bits_per_sample="32bit", + ).extend( + { + cv.Optional(CONF_CORRECT_DC_OFFSET, default=False): cv.boolean, + } ) ).extend(cv.COMPONENT_SCHEMA) - CONFIG_SCHEMA = cv.All( cv.typed_schema( { @@ -140,3 +144,5 @@ async def to_code(config): else: cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_pdm(config[CONF_PDM])) + + cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET])) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 72d1e4476c..2ff1daa197 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -12,6 +12,8 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/components/audio/audio.h" + namespace esphome { namespace i2s_audio { @@ -22,6 +24,9 @@ static const uint32_t READ_DURATION_MS = 16; static const size_t TASK_STACK_SIZE = 4096; static const ssize_t TASK_PRIORITY = 23; +// Use an exponential moving average to correct a DC offset with weight factor 1/1000 +static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000; + static const char *const TAG = "i2s_audio.microphone"; enum MicrophoneEventGroupBits : uint32_t { @@ -70,21 +75,11 @@ void I2SAudioMicrophone::setup() { this->mark_failed(); return; } + + this->configure_stream_settings_(); } -void I2SAudioMicrophone::start() { - if (this->is_failed()) - return; - - xSemaphoreTake(this->active_listeners_semaphore_, 0); -} - -bool I2SAudioMicrophone::start_driver_() { - if (!this->parent_->try_lock()) { - return false; // Waiting for another i2s to return lock - } - esp_err_t err; - +void I2SAudioMicrophone::configure_stream_settings_() { uint8_t channel_count = 1; #ifdef USE_I2S_LEGACY uint8_t bits_per_sample = this->bits_per_sample_; @@ -93,10 +88,10 @@ bool I2SAudioMicrophone::start_driver_() { channel_count = 2; } #else - if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_AUTO) { - this->slot_bit_width_ = I2S_SLOT_BIT_WIDTH_16BIT; + uint8_t bits_per_sample = 16; + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { + bits_per_sample = this->slot_bit_width_; } - uint8_t bits_per_sample = this->slot_bit_width_; if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) { channel_count = 2; @@ -114,6 +109,26 @@ bool I2SAudioMicrophone::start_driver_() { } #endif + if (this->pdm_) { + bits_per_sample = 16; // PDM mics are always 16 bits per sample + } + + this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_); +} + +void I2SAudioMicrophone::start() { + if (this->is_failed()) + return; + + xSemaphoreTake(this->active_listeners_semaphore_, 0); +} + +bool I2SAudioMicrophone::start_driver_() { + if (!this->parent_->try_lock()) { + return false; // Waiting for another i2s to return lock + } + esp_err_t err; + #ifdef USE_I2S_LEGACY i2s_driver_config_t config = { .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), @@ -202,8 +217,6 @@ bool I2SAudioMicrophone::start_driver_() { i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); #if SOC_I2S_SUPPORTS_PDM_RX if (this->pdm_) { - bits_per_sample = 16; // PDM mics are always 16 bits per sample with the IDF 5 driver - i2s_pdm_rx_clk_config_t clk_cfg = { .sample_rate_hz = this->sample_rate_, .clk_src = clk_src, @@ -277,10 +290,8 @@ bool I2SAudioMicrophone::start_driver_() { } #endif - this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_); - this->status_clear_error(); - + this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation return true; } @@ -361,9 +372,12 @@ void I2SAudioMicrophone::mic_task(void *params) { samples.resize(bytes_to_read); size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS)); samples.resize(bytes_read); + if (this_microphone->correct_dc_offset_) { + this_microphone->fix_dc_offset_(samples); + } this_microphone->data_callbacks_.call(samples); } else { - delay(READ_DURATION_MS); + vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS)); } } } @@ -373,11 +387,34 @@ void I2SAudioMicrophone::mic_task(void *params) { xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED); while (true) { - // Continuously delay until the loop method delete the task - delay(10); + // Continuously delay until the loop method deletes the task + vTaskDelay(pdMS_TO_TICKS(10)); } } +void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { + const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1); + const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size()); + + if (total_samples == 0) { + return; + } + + int64_t offset_accumulator = 0; + for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { + const uint32_t byte_index = sample_index * bytes_per_sample; + int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); + offset_accumulator += sample; + sample -= this->dc_offset_; + audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample); + } + + const int32_t new_offset = offset_accumulator / total_samples; + this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR + + (DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ / + DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR; +} + size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { size_t bytes_read = 0; #ifdef USE_I2S_LEGACY diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 8e6d83cad3..39249e879b 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -7,8 +7,10 @@ #include "esphome/components/microphone/microphone.h" #include "esphome/core/component.h" +#include #include #include +#include namespace esphome { namespace i2s_audio { @@ -20,6 +22,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void stop() override; void loop() override; + + void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; } + #ifdef USE_I2S_LEGACY void set_din_pin(int8_t pin) { this->din_pin_ = pin; } #else @@ -41,8 +46,16 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool start_driver_(); void stop_driver_(); + /// @brief Attempts to correct a microphone DC offset; e.g., a microphones silent level is offset from 0. Applies a + /// correction offset that is updated using an exponential moving average for all samples away from 0. + /// @param data + void fix_dc_offset_(std::vector &data); + size_t read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait); + /// @brief Sets the Microphone ``audio_stream_info_`` member variable to the configured I2S settings. + void configure_stream_settings_(); + static void mic_task(void *params); SemaphoreHandle_t active_listeners_semaphore_{nullptr}; @@ -61,6 +74,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub i2s_chan_handle_t rx_handle_; #endif bool pdm_{false}; + + bool correct_dc_offset_; + int32_t dc_offset_{0}; }; } // namespace i2s_audio diff --git a/tests/components/microphone/common.yaml b/tests/components/microphone/common.yaml index d8e4abd12a..00d33bcc3d 100644 --- a/tests/components/microphone/common.yaml +++ b/tests/components/microphone/common.yaml @@ -10,6 +10,7 @@ microphone: adc_type: external pdm: false mclk_multiple: 384 + correct_dc_offset: true on_data: - if: condition: From dcd786d21c624139ee9f406385bf458b10cfcba4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 07:43:38 +1200 Subject: [PATCH 054/193] [config] Deprecate other *_SCHEMA constants (#8748) --- esphome/components/binary_sensor/__init__.py | 9 +++++++-- esphome/components/button/__init__.py | 9 +++++++-- esphome/components/event/__init__.py | 9 +++++++-- esphome/components/number/__init__.py | 9 +++++++-- esphome/components/select/__init__.py | 9 +++++++-- esphome/components/sensor/__init__.py | 9 +++++++-- esphome/components/switch/__init__.py | 4 +++- esphome/components/text_sensor/__init__.py | 9 +++++++-- 8 files changed, 52 insertions(+), 15 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index d0fed9a9b8..448323da5a 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -386,7 +386,7 @@ def validate_click_timing(value): return value -BINARY_SENSOR_SCHEMA = ( +_BINARY_SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) .extend( @@ -480,7 +480,12 @@ def binary_sensor_schema( if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator - return BINARY_SENSOR_SCHEMA.extend(schema) + return _BINARY_SENSOR_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +BINARY_SENSOR_SCHEMA = binary_sensor_schema() +BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) async def setup_binary_sensor_core_(var, config): diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 0307fd3f03..b68334dd98 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -44,7 +44,7 @@ ButtonPressTrigger = button_ns.class_( validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -BUTTON_SCHEMA = ( +_BUTTON_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( @@ -78,7 +78,12 @@ def button_schema( if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator - return BUTTON_SCHEMA.extend(schema) + return _BUTTON_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +BUTTON_SCHEMA = button_schema(Button) +BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) async def setup_button_core_(var, config): diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 99a698e8eb..0e5fb43690 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -41,7 +41,7 @@ EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template()) validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -EVENT_SCHEMA = ( +_EVENT_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) .extend( @@ -79,7 +79,12 @@ def event_schema( if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator - return EVENT_SCHEMA.extend(schema) + return _EVENT_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +EVENT_SCHEMA = event_schema() +EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) async def setup_event_core_(var, config, *, event_types: list[str]): diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 0f68065805..7aa103e9d9 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -170,7 +170,7 @@ NUMBER_OPERATION_OPTIONS = { validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_unit_of_measurement = cv.string_strict -NUMBER_SCHEMA = ( +_NUMBER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( @@ -216,7 +216,12 @@ def number_schema( if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator - return NUMBER_SCHEMA.extend(schema) + return _NUMBER_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +NUMBER_SCHEMA = number_schema(Number) +NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) async def setup_number_core_( diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 106571d94d..2b31ae36d8 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -48,7 +48,7 @@ SELECT_OPERATION_OPTIONS = { } -SELECT_SCHEMA = ( +_SELECT_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( @@ -84,7 +84,12 @@ def select_schema( ) if icon is not cv.UNDEFINED: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - return SELECT_SCHEMA.extend(schema) + return _SELECT_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +SELECT_SCHEMA = select_schema() +SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) async def setup_select_core_(var, config, *, options: list[str]): diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 1eb2d67d6e..051098f6e4 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -264,7 +264,7 @@ validate_accuracy_decimals = cv.int_ validate_icon = cv.icon validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -SENSOR_SCHEMA = ( +_SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) .extend( @@ -337,7 +337,12 @@ def sensor_schema( if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator - return SENSOR_SCHEMA.extend(schema) + return _SENSOR_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +SENSOR_SCHEMA = sensor_schema() +SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor")) @FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index fa69cf1440..1c65aa8dfc 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -135,7 +135,9 @@ def switch_schema( return schema -SWITCH_SCHEMA = switch_schema() # for compatibility +# Remove before 2025.11.0 +SWITCH_SCHEMA = switch_schema() +SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) async def setup_switch_core_(var, config): diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 046af2bd26..92b08aa6d0 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -125,7 +125,7 @@ async def map_filter_to_code(config, filter_id): validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -TEXT_SENSOR_SCHEMA = ( +_TEXT_SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) .extend( @@ -160,7 +160,7 @@ def text_sensor_schema( entity_category: str = cv.UNDEFINED, device_class: str = cv.UNDEFINED, ) -> cv.Schema: - schema = TEXT_SENSOR_SCHEMA + schema = _TEXT_SENSOR_SCHEMA if class_ is not cv.UNDEFINED: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if icon is not cv.UNDEFINED: @@ -184,6 +184,11 @@ def text_sensor_schema( return schema +# Remove before 2025.11.0 +TEXT_SENSOR_SCHEMA = text_sensor_schema() +TEXT_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("text_sensor")) + + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) From 38790793ddb350efa19df82b63d15e341a915640 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 08:53:46 +1200 Subject: [PATCH 055/193] [opentherm] Update to use schema methods (#8756) --- .../opentherm/binary_sensor/__init__.py | 6 ++-- .../components/opentherm/number/__init__.py | 31 +++++++------------ .../components/opentherm/switch/__init__.py | 8 ++--- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/esphome/components/opentherm/binary_sensor/__init__.py b/esphome/components/opentherm/binary_sensor/__init__.py index ce5a701a53..35228228ea 100644 --- a/esphome/components/opentherm/binary_sensor/__init__.py +++ b/esphome/components/opentherm/binary_sensor/__init__.py @@ -11,10 +11,8 @@ COMPONENT_TYPE = const.BINARY_SENSOR def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema: return binary_sensor.binary_sensor_schema( - device_class=( - entity.device_class or cv.UNDEFINED # pylint: disable=protected-access - ), - icon=(entity.icon or cv.UNDEFINED), # pylint: disable=protected-access + device_class=(entity.device_class or cv.UNDEFINED), + icon=(entity.icon or cv.UNDEFINED), ) diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py index 00aa62483c..a65864647a 100644 --- a/esphome/components/opentherm/number/__init__.py +++ b/esphome/components/opentherm/number/__init__.py @@ -3,13 +3,7 @@ from typing import Any import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv -from esphome.const import ( - CONF_ID, - CONF_INITIAL_VALUE, - CONF_RESTORE_VALUE, - CONF_STEP, - CONF_UNIT_OF_MEASUREMENT, -) +from esphome.const import CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_STEP from .. import const, generate, input, schema, validate @@ -22,33 +16,30 @@ OpenthermNumber = generate.opentherm_ns.class_( async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await number.register_number( - var, + var = await number.new_number( config, min_value=config[input.CONF_min_value], max_value=config[input.CONF_max_value], step=config[input.CONF_step], ) + await cg.register_component(var, config) input.generate_setters(var, config) - if CONF_INITIAL_VALUE in config: - cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) - if CONF_RESTORE_VALUE in config: - cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + if (initial_value := config.get(CONF_INITIAL_VALUE, None)) is not None: + cg.add(var.set_initial_value(initial_value)) + if (restore_value := config.get(CONF_RESTORE_VALUE, None)) is not None: + cg.add(var.set_restore_value(restore_value)) return var def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema: return ( - number.NUMBER_SCHEMA.extend( + number.number_schema( + OpenthermNumber, unit_of_measurement=entity.unit_of_measurement + ) + .extend( { - cv.GenerateID(): cv.declare_id(OpenthermNumber), - cv.Optional( - CONF_UNIT_OF_MEASUREMENT, entity.unit_of_measurement - ): cv.string_strict, cv.Optional(CONF_STEP, entity.step): cv.float_, cv.Optional(CONF_INITIAL_VALUE): cv.float_, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, diff --git a/esphome/components/opentherm/switch/__init__.py b/esphome/components/opentherm/switch/__init__.py index ead086d24b..f8f09b3967 100644 --- a/esphome/components/opentherm/switch/__init__.py +++ b/esphome/components/opentherm/switch/__init__.py @@ -3,7 +3,6 @@ from typing import Any import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv -from esphome.const import CONF_ID from .. import const, generate, schema, validate @@ -16,15 +15,14 @@ OpenthermSwitch = generate.opentherm_ns.class_( async def new_openthermswitch(config: dict[str, Any]) -> cg.Pvariable: - var = cg.new_Pvariable(config[CONF_ID]) + var = await switch.new_switch(config) await cg.register_component(var, config) - await switch.register_switch(var, config) return var def get_entity_validation_schema(entity: schema.SwitchSchema) -> cv.Schema: - return switch.SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(OpenthermSwitch)} + return switch.switch_schema( + OpenthermSwitch, default_restore_mode=entity.default_mode ).extend(cv.COMPONENT_SCHEMA) From bdd2774544fd11d3ec649b8e3e91a162ee9a3b47 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 08:58:05 +1200 Subject: [PATCH 056/193] [factory_reset] Use switch_schema method (#8757) --- .../factory_reset/switch/__init__.py | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/esphome/components/factory_reset/switch/__init__.py b/esphome/components/factory_reset/switch/__init__.py index 17f4587e5d..a384a57f80 100644 --- a/esphome/components/factory_reset/switch/__init__.py +++ b/esphome/components/factory_reset/switch/__init__.py @@ -1,14 +1,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv -from esphome.const import ( - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_ID, - CONF_INVERTED, - ENTITY_CATEGORY_CONFIG, - ICON_RESTART_ALERT, -) +from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT from .. import factory_reset_ns @@ -16,21 +9,14 @@ FactoryResetSwitch = factory_reset_ns.class_( "FactoryResetSwitch", switch.Switch, cg.Component ) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(FactoryResetSwitch), - cv.Optional(CONF_INVERTED): cv.invalid( - "Factory Reset switches do not support inverted mode!" - ), - cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): cv.icon, - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG - ): cv.entity_category, - } +CONFIG_SCHEMA = switch.switch_schema( + FactoryResetSwitch, + block_inverted=True, + icon=ICON_RESTART_ALERT, + entity_category=ENTITY_CATEGORY_CONFIG, ).extend(cv.COMPONENT_SCHEMA) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await switch.new_switch(config) await cg.register_component(var, config) - await switch.register_switch(var, config) From cff18207729729c9e3bdb7d59aa421d128eba08d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 08:59:42 +1200 Subject: [PATCH 057/193] [sprinkler] Use number_schema method (#8759) --- esphome/components/sprinkler/__init__.py | 37 ++++++++++++------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 2c59309b1f..3c94d97739 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -4,7 +4,6 @@ import esphome.codegen as cg from esphome.components import number, switch import esphome.config_validation as cv from esphome.const import ( - CONF_ENTITY_CATEGORY, CONF_ID, CONF_INITIAL_VALUE, CONF_MAX_VALUE, @@ -296,12 +295,11 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds, cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value( - number.NUMBER_SCHEMA.extend( + number.number_schema( + SprinklerControllerNumber, entity_category=ENTITY_CATEGORY_CONFIG + ) + .extend( { - cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG - ): cv.entity_category, cv.Optional(CONF_INITIAL_VALUE, default=900): cv.positive_int, cv.Optional(CONF_MAX_VALUE, default=86400): cv.positive_int, cv.Optional(CONF_MIN_VALUE, default=1): cv.positive_int, @@ -314,7 +312,8 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_min_max, key=CONF_NAME, ), @@ -371,12 +370,11 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( cv.Optional(CONF_NEXT_PREV_IGNORE_DISABLED, default=False): cv.boolean, cv.Optional(CONF_MANUAL_SELECTION_DELAY): cv.positive_time_period_seconds, cv.Optional(CONF_MULTIPLIER_NUMBER): cv.maybe_simple_value( - number.NUMBER_SCHEMA.extend( + number.number_schema( + SprinklerControllerNumber, entity_category=ENTITY_CATEGORY_CONFIG + ) + .extend( { - cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG - ): cv.entity_category, cv.Optional(CONF_INITIAL_VALUE, default=1): cv.positive_float, cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_float, cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_float, @@ -386,18 +384,18 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( single=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_min_max, key=CONF_NAME, ), cv.Optional(CONF_REPEAT): cv.positive_int, cv.Optional(CONF_REPEAT_NUMBER): cv.maybe_simple_value( - number.NUMBER_SCHEMA.extend( + number.number_schema( + SprinklerControllerNumber, entity_category=ENTITY_CATEGORY_CONFIG + ) + .extend( { - cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG - ): cv.entity_category, cv.Optional(CONF_INITIAL_VALUE, default=0): cv.positive_int, cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_int, cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_int, @@ -407,7 +405,8 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( single=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_min_max, key=CONF_NAME, ), From a3ed0905942b2111dd0ec456fcfc81246ab15eb0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 08:59:59 +1200 Subject: [PATCH 058/193] [tm1638] Use switch_schema method (#8758) --- esphome/components/tm1638/switch/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/esphome/components/tm1638/switch/__init__.py b/esphome/components/tm1638/switch/__init__.py index 8832cf8b92..90ff87938c 100644 --- a/esphome/components/tm1638/switch/__init__.py +++ b/esphome/components/tm1638/switch/__init__.py @@ -8,13 +8,16 @@ from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638SwitchLed = tm1638_ns.class_("TM1638SwitchLed", switch.Switch, cg.Component) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TM1638SwitchLed), - cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component), - cv.Required(CONF_LED): cv.int_range(min=0, max=7), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + switch.switch_schema(TM1638SwitchLed) + .extend( + { + cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component), + cv.Required(CONF_LED): cv.int_range(min=0, max=7), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): From 81bae96109810223c85e3ba190dae42563632c58 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 09:05:23 +1200 Subject: [PATCH 059/193] [airthings] Remove unnecessary schema (#8760) --- esphome/components/airthings_wave_base/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/airthings_wave_base/__init__.py b/esphome/components/airthings_wave_base/__init__.py index 6a29683ced..c3f3b8f199 100644 --- a/esphome/components/airthings_wave_base/__init__.py +++ b/esphome/components/airthings_wave_base/__init__.py @@ -34,7 +34,7 @@ AirthingsWaveBase = airthings_wave_base_ns.class_( BASE_SCHEMA = ( - sensor.SENSOR_SCHEMA.extend( + cv.Schema( { cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, From 50c88b7aa797fdc70131febcac5748e4028cac5a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 09:15:57 +1200 Subject: [PATCH 060/193] [ble_client] Use text_sensor_schema method (#8761) --- esphome/components/ble_client/text_sensor/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index afa60f6c0c..a6b8956f93 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -4,7 +4,6 @@ from esphome.components import ble_client, esp32_ble_tracker, text_sensor import esphome.config_validation as cv from esphome.const import ( CONF_CHARACTERISTIC_UUID, - CONF_ID, CONF_NOTIFY, CONF_SERVICE_UUID, CONF_TRIGGER_ID, @@ -32,9 +31,9 @@ BLETextSensorNotifyTrigger = ble_client_ns.class_( ) CONFIG_SCHEMA = cv.All( - text_sensor.TEXT_SENSOR_SCHEMA.extend( + text_sensor.text_sensor_schema(BLETextSensor) + .extend( { - cv.GenerateID(): cv.declare_id(BLETextSensor), cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, @@ -54,7 +53,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) @@ -101,7 +100,6 @@ async def to_code(config): await cg.register_component(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_enable_notify(config[CONF_NOTIFY])) - await text_sensor.register_text_sensor(var, config) for conf in config.get(CONF_ON_NOTIFY, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await ble_client.register_ble_node(trigger, config) From 58104229e2ad2fb4ff9a14decf77c351670fa44a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 09:16:56 +1200 Subject: [PATCH 061/193] [sml] Use text_sensor_schema method (#8762) --- esphome/components/sml/text_sensor/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/sml/text_sensor/__init__.py b/esphome/components/sml/text_sensor/__init__.py index 401db9c582..9c9da26c3a 100644 --- a/esphome/components/sml/text_sensor/__init__.py +++ b/esphome/components/sml/text_sensor/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv -from esphome.const import CONF_FORMAT, CONF_ID +from esphome.const import CONF_FORMAT from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns @@ -19,9 +19,8 @@ SML_TYPES = { SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = text_sensor.text_sensor_schema(SmlTextSensor).extend( { - cv.GenerateID(): cv.declare_id(SmlTextSensor), cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), cv.Required(CONF_OBIS_CODE): obis_code, cv.Optional(CONF_SERVER_ID, default=""): cv.string, @@ -31,13 +30,12 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable( - config[CONF_ID], + var = await text_sensor.new_text_sensor( + config, config[CONF_SERVER_ID], config[CONF_OBIS_CODE], config[CONF_FORMAT], ) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) sml = await cg.get_variable(config[CONF_SML_ID]) cg.add(sml.register_sml_listener(var)) From facf94699e1287fdab02d709b23ed43059317294 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 12 May 2025 23:17:28 +0200 Subject: [PATCH 062/193] [udp, syslog] fix clang tidy (#8755) --- esphome/components/syslog/esphome_syslog.h | 2 ++ esphome/components/udp/automation.h | 2 ++ esphome/components/udp/packet_transport/udp_transport.h | 2 ++ 3 files changed, 6 insertions(+) diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index 3fa077b466..421a9bee73 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -5,6 +5,7 @@ #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" +#ifdef USE_NETWORK namespace esphome { namespace syslog { class Syslog : public Component, public Parented { @@ -23,3 +24,4 @@ class Syslog : public Component, public Parented { }; } // namespace syslog } // namespace esphome +#endif diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index 663daa1c15..f75e6d35bf 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -1,6 +1,7 @@ #pragma once #include "udp_component.h" +#ifdef USE_NETWORK #include "esphome/core/automation.h" #include @@ -36,3 +37,4 @@ template class UDPWriteAction : public Action, public Par } // namespace udp } // namespace esphome +#endif diff --git a/esphome/components/udp/packet_transport/udp_transport.h b/esphome/components/udp/packet_transport/udp_transport.h index 4a95a095cc..c87eb62780 100644 --- a/esphome/components/udp/packet_transport/udp_transport.h +++ b/esphome/components/udp/packet_transport/udp_transport.h @@ -1,6 +1,7 @@ #pragma once #include "../udp_component.h" +#ifdef USE_NETWORK #include "esphome/core/component.h" #include "esphome/components/packet_transport/packet_transport.h" #include @@ -24,3 +25,4 @@ class UDPTransport : public packet_transport::PacketTransport, public Parented Date: Tue, 13 May 2025 09:31:40 +1200 Subject: [PATCH 063/193] Bump zeroconf from 0.146.5 to 0.147.0 (#8754) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f91d86f75b..9547cd0ef0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==4.8.1 click==8.1.7 esphome-dashboard==20250415.0 aioesphomeapi==30.2.0 -zeroconf==0.146.5 +zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import esphome-glyphsets==0.2.0 From 5b2c19bc86a293dbbfe81894f8b55bb34d703e65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 09:32:13 +1200 Subject: [PATCH 064/193] Bump setuptools from 80.3.1 to 80.4.0 (#8753) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1910f008ce..1971f033c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.3.1", "wheel>=0.43,<0.46"] +requires = ["setuptools==80.4.0", "wheel>=0.43,<0.46"] build-backend = "setuptools.build_meta" [project] From f4eb75e4e07551e1c76ab9c449c2cbfbdc5cd1e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 May 2025 17:29:50 -0500 Subject: [PATCH 065/193] Avoid iterating clients twice in the api_server loop (#8733) --- esphome/components/api/api_server.cpp | 34 +++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 9a594c9223..b987b44705 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -126,19 +126,29 @@ void APIServer::loop() { conn->start(); } - // Partition clients into remove and active - auto new_end = std::partition(this->clients_.begin(), this->clients_.end(), - [](const std::unique_ptr &conn) { return !conn->remove_; }); - // print disconnection messages - for (auto it = new_end; it != this->clients_.end(); ++it) { - this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_); - ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); - } - // resize vector - this->clients_.erase(new_end, this->clients_.end()); + // Process clients and remove disconnected ones in a single pass + if (!this->clients_.empty()) { + size_t client_index = 0; + while (client_index < this->clients_.size()) { + auto &client = this->clients_[client_index]; - for (auto &client : this->clients_) { - client->loop(); + if (client->remove_) { + // Handle disconnection + this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); + ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str()); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + // Don't increment client_index since we need to process the swapped element + } else { + // Process active client + client->loop(); + client_index++; // Move to next client + } + } } if (this->reboot_timeout_ != 0) { From 7c0546c9f0681f15da7cde9b2509945a14e9cfe6 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 May 2025 01:36:34 +0200 Subject: [PATCH 066/193] [clang] clang tidy support with zephyr (#8352) Co-authored-by: Keith Burzinski --- .github/workflows/ci.yml | 9 ++- esphome/components/spi/spi.h | 6 +- esphome/core/defines.h | 34 +++++----- platformio.ini | 36 ++++++++++ script/ci-custom.py | 1 + script/clang-tidy | 59 ++++++++++------- script/helpers.py | 13 ++-- script/helpers_zephyr.py | 124 +++++++++++++++++++++++++++++++++++ 8 files changed, 237 insertions(+), 45 deletions(-) create mode 100644 script/helpers_zephyr.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77fe79fd1d..8d2ec68010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -292,6 +292,11 @@ jobs: name: Run script/clang-tidy for ESP32 IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF pio_cache_key: tidyesp32-idf + - id: clang-tidy + name: Run script/clang-tidy for ZEPHYR + options: --environment nrf52-tidy --grep USE_ZEPHYR + pio_cache_key: tidy-zephyr + ignore_errors: true steps: - name: Check out code from GitHub @@ -331,13 +336,13 @@ jobs: - name: Run clang-tidy run: | . venv/bin/activate - script/clang-tidy --all-headers --fix ${{ matrix.options }} + script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} env: # Also cache libdeps, store them in a ~/.platformio subfolder PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps - name: Suggested changes - run: script/ci-suggest-changes + run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }} # yamllint disable-line rule:line-length if: always() diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 378d95e7b9..7cdffafdb5 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,5 +1,4 @@ #pragma once - #include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" @@ -28,6 +27,11 @@ using SPIInterface = spi_host_device_t; #endif // USE_ESP_IDF +#ifdef USE_ZEPHYR +// TODO supprse clang-tidy. Remove after SPI driver for nrf52 is added. +using SPIInterface = void *; +#endif + /** * Implementation of SPI Controller mode. */ diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9f4099e67f..8bc554d5f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -20,11 +20,6 @@ // Feature flags #define USE_ALARM_CONTROL_PANEL -#define USE_AUDIO_FLAC_SUPPORT -#define USE_AUDIO_MP3_SUPPORT -#define USE_API -#define USE_API_NOISE -#define USE_API_PLAINTEXT #define USE_BINARY_SENSOR #define USE_BUTTON #define USE_CLIMATE @@ -79,20 +74,10 @@ #define USE_LVGL_TEXTAREA #define USE_LVGL_TILEVIEW #define USE_LVGL_TOUCHSCREEN -#define USE_MD5 #define USE_MDNS #define USE_MEDIA_PLAYER -#define USE_MQTT -#define USE_NETWORK #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER -#define USE_ONLINE_IMAGE_BMP_SUPPORT -#define USE_ONLINE_IMAGE_PNG_SUPPORT -#define USE_ONLINE_IMAGE_JPEG_SUPPORT -#define USE_OTA -#define USE_OTA_PASSWORD -#define USE_OTA_STATE_CALLBACK -#define USE_OTA_VERSION 2 #define USE_OUTPUT #define USE_POWER_SUPPLY #define USE_QR_CODE @@ -107,9 +92,28 @@ #define USE_UART_DEBUGGER #define USE_UPDATE #define USE_VALVE + +// Feature flags which do not work for zephyr +#ifndef USE_ZEPHYR +#define USE_AUDIO_FLAC_SUPPORT +#define USE_AUDIO_MP3_SUPPORT +#define USE_API +#define USE_API_NOISE +#define USE_API_PLAINTEXT +#define USE_MD5 +#define USE_MQTT +#define USE_NETWORK +#define USE_ONLINE_IMAGE_BMP_SUPPORT +#define USE_ONLINE_IMAGE_PNG_SUPPORT +#define USE_ONLINE_IMAGE_JPEG_SUPPORT +#define USE_OTA +#define USE_OTA_PASSWORD +#define USE_OTA_STATE_CALLBACK +#define USE_OTA_VERSION 2 #define USE_WIFI #define USE_WIFI_AP #define USE_WIREGUARD +#endif // Arduino-specific feature flags #ifdef USE_ARDUINO diff --git a/platformio.ini b/platformio.ini index 61b7f8d746..ccfd52c3ca 100644 --- a/platformio.ini +++ b/platformio.ini @@ -194,6 +194,26 @@ build_flags = -DUSE_LIBRETINY build_src_flags = -include Arduino.h +; This is the common settings for the nRF52 using Zephyr. +[common:nrf52-zephyr] +extends = common +platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip +framework = zephyr +platform_packages = + platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip + platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip +build_flags = + ${common.build_flags} + -DUSE_ZEPHYR + -DUSE_NRF52 +lib_deps = + bblanchon/ArduinoJson@7.0.0 ; json + wjtje/qr-code-generator-library@1.7.0 ; qr_code + pavlodn/HaierProtocol@0.9.31 ; haier + functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 + https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library + lvgl/lvgl@8.4.0 ; lvgl + ; All the actual environments are defined below. ;;;;;;;; ESP8266 ;;;;;;;; @@ -440,3 +460,19 @@ build_flags = ${common.build_flags} -DUSE_HOST -std=c++17 + +;;;;;;;; nRF52 ;;;;;;;; + +[env:nrf52] +extends = common:nrf52-zephyr +board = adafruit_feather_nrf52840 +build_flags = + ${common:nrf52-zephyr.build_flags} + ${flags:runtime.build_flags} + +[env:nrf52-tidy] +extends = common:nrf52-zephyr +board = adafruit_feather_nrf52840 +build_flags = + ${common:nrf52-zephyr.build_flags} + ${flags:clangtidy.build_flags} diff --git a/script/ci-custom.py b/script/ci-custom.py index dda5410778..a3a31b2259 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -558,6 +558,7 @@ def lint_relative_py_import(fname): "esphome/components/rp2040/core.cpp", "esphome/components/libretiny/core.cpp", "esphome/components/host/core.cpp", + "esphome/components/zephyr/core.cpp", "esphome/components/http_request/httplib.h", ], ) diff --git a/script/clang-tidy b/script/clang-tidy index a857274b01..5baaaf6b3a 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -40,12 +40,37 @@ def clang_options(idedata): else: cmd.append(f"--target={triplet}") + omit_flags = ( + "-free", + "-fipa-pta", + "-fstrict-volatile-bitfields", + "-mlongcalls", + "-mtext-section-literals", + "-mdisable-hardware-atomics", + "-mfix-esp32-psram-cache-issue", + "-mfix-esp32-psram-cache-strategy=memw", + "-fno-tree-switch-conversion", + ) + + if "zephyr" in triplet: + omit_flags += ( + "-fno-reorder-functions", + "-mfp16-format=ieee", + "--param=min-pagesize=0", + ) + else: + cmd.extend( + [ + # disable built-in include directories from the host + "-nostdinc++", + ] + ) + # set flags cmd.extend( [ # disable built-in include directories from the host "-nostdinc", - "-nostdinc++", # replace pgmspace.h, as it uses GNU extensions clang doesn't support # https://github.com/earlephilhower/newlib-xtensa/pull/18 "-D_PGMSPACE_H_", @@ -70,22 +95,7 @@ def clang_options(idedata): ) # copy compiler flags, except those clang doesn't understand. - cmd.extend( - flag - for flag in idedata["cxx_flags"] - if flag - not in ( - "-free", - "-fipa-pta", - "-fstrict-volatile-bitfields", - "-mlongcalls", - "-mtext-section-literals", - "-mdisable-hardware-atomics", - "-mfix-esp32-psram-cache-issue", - "-mfix-esp32-psram-cache-strategy=memw", - "-fno-tree-switch-conversion", - ) - ) + cmd.extend(flag for flag in idedata["cxx_flags"] if flag not in omit_flags) # defines cmd.extend(f"-D{define}" for define in idedata["defines"]) @@ -100,13 +110,16 @@ def clang_options(idedata): # add library include directories using -isystem to suppress their errors for directory in list(idedata["includes"]["build"]): # skip our own directories, we add those later - if not directory.startswith(f"{root_path}") or directory.startswith( - ( - f"{root_path}/.pio", - f"{root_path}/.platformio", - f"{root_path}/.temp", - f"{root_path}/managed_components", + if ( + not directory.startswith(f"{root_path}") + or directory.startswith( + ( + f"{root_path}/.platformio", + f"{root_path}/.temp", + f"{root_path}/managed_components", + ) ) + or (directory.startswith(f"{root_path}") and "/.pio/" in directory) ): cmd.extend(["-isystem", directory]) diff --git a/script/helpers.py b/script/helpers.py index 6148371e32..3c1b0c0ddd 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -5,6 +5,7 @@ import re import subprocess import colorama +import helpers_zephyr root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") @@ -147,10 +148,14 @@ def load_idedata(environment): # ensure temp directory exists before running pio, as it writes sdkconfig to it Path(temp_folder).mkdir(exist_ok=True) - stdout = subprocess.check_output(["pio", "run", "-t", "idedata", "-e", environment]) - match = re.search(r'{\s*".*}', stdout.decode("utf-8")) - data = json.loads(match.group()) - + if "nrf" in environment: + data = helpers_zephyr.load_idedata(environment, temp_folder, platformio_ini) + else: + stdout = subprocess.check_output( + ["pio", "run", "-t", "idedata", "-e", environment] + ) + match = re.search(r'{\s*".*}', stdout.decode("utf-8")) + data = json.loads(match.group()) temp_idedata.write_text(json.dumps(data, indent=2) + "\n") return data diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py new file mode 100644 index 0000000000..c3ba149005 --- /dev/null +++ b/script/helpers_zephyr.py @@ -0,0 +1,124 @@ +import json +from pathlib import Path +import re +import subprocess + + +def load_idedata(environment, temp_folder, platformio_ini): + build_environment = environment.replace("-tidy", "") + build_dir = Path(temp_folder) / f"build-{build_environment}" + Path(build_dir).mkdir(exist_ok=True) + Path(build_dir / "platformio.ini").write_text( + Path(platformio_ini).read_text(encoding="utf-8"), encoding="utf-8" + ) + esphome_dir = Path(build_dir / "esphome") + esphome_dir.mkdir(exist_ok=True) + Path(esphome_dir / "main.cpp").write_text( + """ +#include +int main() { return 0;} +""", + encoding="utf-8", + ) + zephyr_dir = Path(build_dir / "zephyr") + zephyr_dir.mkdir(exist_ok=True) + Path(zephyr_dir / "prj.conf").write_text( + """ +CONFIG_NEWLIB_LIBC=y +""", + encoding="utf-8", + ) + subprocess.run(["pio", "run", "-e", build_environment, "-d", build_dir], check=True) + + def extract_include_paths(command): + include_paths = [] + include_pattern = re.compile(r'("-I\s*[^"]+)|(-isystem\s*[^\s]+)|(-I\s*[^\s]+)') + for match in include_pattern.findall(command): + split_strings = re.split( + r"\s*-\s*(?:I|isystem)", list(filter(lambda x: x, match))[0] + ) + include_paths.append(split_strings[1]) + return include_paths + + def extract_defines(command): + defines = [] + define_pattern = re.compile(r"-D\s*([^\s]+)") + for match in define_pattern.findall(command): + if match not in ("_ASMLANGUAGE"): + defines.append(match) + return defines + + def find_cxx_path(commands): + for entry in commands: + command = entry["command"] + cxx_path = command.split()[0] + if not cxx_path.endswith("++"): + continue + return cxx_path + + def get_builtin_include_paths(compiler): + result = subprocess.run( + [compiler, "-E", "-x", "c++", "-", "-v"], + input="", + text=True, + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + check=True, + ) + include_paths = [] + start_collecting = False + for line in result.stderr.splitlines(): + if start_collecting: + if line.startswith(" "): + include_paths.append(line.strip()) + else: + break + if "#include <...> search starts here:" in line: + start_collecting = True + return include_paths + + def extract_cxx_flags(command): + flags = [] + # Extracts CXXFLAGS from the command string, excluding includes and defines. + flag_pattern = re.compile( + r"(-O[0-3s]|-g|-std=[^\s]+|-Wall|-Wextra|-Werror|--[^\s]+|-f[^\s]+|-m[^\s]+|-imacros\s*[^\s]+)" + ) + for match in flag_pattern.findall(command): + flags.append(match.replace("-imacros ", "-imacros")) + return flags + + def transform_to_idedata_format(compile_commands): + cxx_path = find_cxx_path(compile_commands) + idedata = { + "includes": { + "toolchain": get_builtin_include_paths(cxx_path), + "build": set(), + }, + "defines": set(), + "cxx_path": cxx_path, + "cxx_flags": set(), + } + + for entry in compile_commands: + command = entry["command"] + exec = command.split()[0] + if exec != cxx_path: + continue + + idedata["includes"]["build"].update(extract_include_paths(command)) + idedata["defines"].update(extract_defines(command)) + idedata["cxx_flags"].update(extract_cxx_flags(command)) + + # Convert sets to lists for JSON serialization + idedata["includes"]["build"] = list(idedata["includes"]["build"]) + idedata["defines"] = list(idedata["defines"]) + idedata["cxx_flags"] = list(idedata["cxx_flags"]) + + return idedata + + compile_commands = json.loads( + Path( + build_dir / ".pio" / "build" / build_environment / "compile_commands.json" + ).read_text(encoding="utf-8") + ) + return transform_to_idedata_format(compile_commands) From a9d5eb84703e5b9dd2b180af4689d999691a6768 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 May 2025 20:17:37 -0500 Subject: [PATCH 067/193] Fix missing recursion guard release on ESP8266 (#8766) --- esphome/components/logger/logger.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 03e42cdd48..812a7cc16d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -72,7 +72,8 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * #endif // !USE_ESP32 #ifdef USE_STORE_LOG_STR_IN_FLASH -// Implementation for ESP8266 with flash string support +// Implementation for ESP8266 with flash string support. +// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || recursion_guard_) @@ -89,8 +90,10 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } // Buffer full from copying format - if (this->tx_buffer_at_ >= this->tx_buffer_size_) + if (this->tx_buffer_at_ >= this->tx_buffer_size_) { + recursion_guard_ = false; // Make sure to reset the recursion guard before returning return; + } // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string @@ -98,7 +101,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // No write console and callback starting at the msg_start + // Write to console and send callback starting at the msg_start if (this->baud_rate_ > 0) { this->write_msg_(this->tx_buffer_ + msg_start); } From 49f631d6c5b7ef20cfa751b4899122abfdb9f21a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 13:18:23 +1200 Subject: [PATCH 068/193] [schema] Deploy schema after release workflow finished (#8767) --- .github/workflows/release.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 359a9bcc53..88704953ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -231,3 +231,25 @@ jobs: content: description } }) + + deploy-esphome-schema: + if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' + runs-on: ubuntu-latest + needs: + - init + - deploy-manifest + steps: + - name: Trigger Workflow + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} + script: | + github.rest.actions.createWorkflowDispatch({ + owner: "esphome", + repo: "esphome-schema", + workflow_id: "generate-schemas.yml", + ref: "main", + inputs: { + version: "${{ needs.init.outputs.tag }}", + } + }) From bc0956019b4638720f967cedc999f5e1f7f02937 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 13:24:13 +1200 Subject: [PATCH 069/193] [config] Deprecate more *_SCHEMA constants (#8763) --- .../alarm_control_panel/__init__.py | 39 ++++++++++++- esphome/components/climate/__init__.py | 37 +++++++++++- esphome/components/cover/__init__.py | 39 ++++++++++++- esphome/components/fan/__init__.py | 41 +++++++++++-- esphome/components/light/__init__.py | 57 +++++++++++++++++++ 5 files changed, 202 insertions(+), 11 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 379fbf32f9..1bcb83bce7 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server import esphome.config_validation as cv from esphome.const import ( CONF_CODE, + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_MQTT_ID, CONF_ON_STATE, @@ -12,6 +14,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@grahambrown11", "@hwstar"] @@ -78,12 +81,11 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_( "AlarmControlPanelCondition", automation.Condition ) -ALARM_CONTROL_PANEL_SCHEMA = ( +_ALARM_CONTROL_PANEL_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( { - cv.GenerateID(): cv.declare_id(AlarmControlPanel), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTAlarmControlPanelComponent ), @@ -146,6 +148,33 @@ ALARM_CONTROL_PANEL_SCHEMA = ( ) ) + +def alarm_control_panel_schema( + class_: MockObjClass, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(class_), + } + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel) +ALARM_CONTROL_PANEL_SCHEMA.add_extra( + cv.deprecated_schema_constant("alarm_control_panel") +) + ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( { cv.GenerateID(): cv.use_id(AlarmControlPanel), @@ -209,6 +238,12 @@ async def register_alarm_control_panel(var, config): await setup_alarm_control_panel_core_(var, config) +async def new_alarm_control_panel(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_alarm_control_panel(var, config) + return var + + @automation.register_action( "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA ) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 445507c620..43c86dcdc2 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -11,9 +11,11 @@ from esphome.const import ( CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, + CONF_ENTITY_CATEGORY, CONF_FAN_MODE, CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_STATE_TOPIC, + CONF_ICON, CONF_ID, CONF_MAX_TEMPERATURE, CONF_MIN_TEMPERATURE, @@ -46,6 +48,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -151,12 +154,11 @@ ControlTrigger = climate_ns.class_( "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref")) ) -CLIMATE_SCHEMA = ( +_CLIMATE_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( { - cv.GenerateID(): cv.declare_id(Climate), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), cv.Optional(CONF_VISUAL, default={}): cv.Schema( { @@ -245,6 +247,31 @@ CLIMATE_SCHEMA = ( ) +def climate_schema( + class_: MockObjClass, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(Climate), + } + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _CLIMATE_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +CLIMATE_SCHEMA = climate_schema(Climate) +CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) + + async def setup_climate_core_(var, config): await setup_entity(var, config) @@ -419,6 +446,12 @@ async def register_climate(var, config): await setup_climate_core_(var, config) +async def new_climate(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_climate(var, config) + return var + + CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.use_id(Climate), diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index e7e3ac3bb0..13f117c3f0 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_MQTT_ID, CONF_ON_OPEN, @@ -31,6 +33,7 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -89,12 +92,11 @@ CoverClosedTrigger = cover_ns.class_( CONF_ON_CLOSED = "on_closed" -COVER_SCHEMA = ( +_COVER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( { - cv.GenerateID(): cv.declare_id(Cover), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( @@ -124,6 +126,33 @@ COVER_SCHEMA = ( ) +def cover_schema( + class_: MockObjClass, + *, + device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(class_), + } + + for key, default, validator in [ + (CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _COVER_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +COVER_SCHEMA = cover_schema(Cover) +COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) + + async def setup_cover_core_(var, config): await setup_entity(var, config) @@ -163,6 +192,12 @@ async def register_cover(var, config): await setup_cover_core_(var, config) +async def new_cover(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_cover(var, config) + return var + + COVER_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(Cover), diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 9c9cb6327b..960809ff70 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -7,6 +7,8 @@ from esphome.const import ( CONF_DIRECTION, CONF_DIRECTION_COMMAND_TOPIC, CONF_DIRECTION_STATE_TOPIC, + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_MQTT_ID, CONF_OFF_SPEED_CYCLE, @@ -82,12 +84,11 @@ FanPresetSetTrigger = fan_ns.class_( FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) -FAN_SCHEMA = ( +_FAN_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend( { - cv.GenerateID(): cv.declare_id(Fan), cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), @@ -159,6 +160,37 @@ FAN_SCHEMA = ( ) ) + +def fan_schema( + class_: cg.Pvariable, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, + default_restore_mode: str = cv.UNDEFINED, +) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(class_), + } + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ( + CONF_RESTORE_MODE, + default_restore_mode, + cv.enum(RESTORE_MODES, upper=True, space="_"), + ), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _FAN_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +FAN_SCHEMA = fan_schema(Fan) +FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan")) + _PRESET_MODES_SCHEMA = cv.All( cv.ensure_list(cv.string_strict), cv.Length(min=1), @@ -267,10 +299,9 @@ async def register_fan(var, config): await setup_fan_core_(var, config) -async def create_fan_state(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_fan(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_fan(var, config) - await cg.register_component(var, config) return var diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index feac385b66..237ab45f38 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,3 +1,5 @@ +import enum + import esphome.automation as auto import esphome.codegen as cg from esphome.components import mqtt, power_supply, web_server @@ -13,15 +15,18 @@ from esphome.const import ( CONF_COLOR_TEMPERATURE, CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, + CONF_ENTITY_CATEGORY, CONF_FLASH_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, CONF_GREEN, + CONF_ICON, CONF_ID, CONF_INITIAL_STATE, CONF_MQTT_ID, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_OUTPUT_ID, CONF_POWER_SUPPLY, CONF_RED, CONF_RESTORE_MODE, @@ -33,6 +38,7 @@ from esphome.const import ( CONF_WHITE, ) from esphome.core import coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity from .automation import LIGHT_STATE_SCHEMA @@ -141,6 +147,51 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( ) +class LightType(enum.IntEnum): + """Light type enum.""" + + BINARY = 0 + BRIGHTNESS_ONLY = 1 + RGB = 2 + ADDRESSABLE = 3 + + +def light_schema( + class_: MockObjClass, + type_: LightType, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, + default_restore_mode: str = cv.UNDEFINED, +) -> cv.Schema: + schema = { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(class_), + } + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ( + CONF_RESTORE_MODE, + default_restore_mode, + cv.enum(RESTORE_MODES, upper=True, space="_"), + ), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + if type_ == LightType.BINARY: + return BINARY_LIGHT_SCHEMA.extend(schema) + if type_ == LightType.BRIGHTNESS_ONLY: + return BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(schema) + if type_ == LightType.RGB: + return RGB_LIGHT_SCHEMA.extend(schema) + if type_ == LightType.ADDRESSABLE: + return ADDRESSABLE_LIGHT_SCHEMA.extend(schema) + + raise ValueError(f"Invalid light type: {type_}") + + def validate_color_temperature_channels(value): if ( CONF_COLD_WHITE_COLOR_TEMPERATURE in value @@ -223,6 +274,12 @@ async def register_light(output_var, config): await setup_light_core_(light_var, output_var, config) +async def new_light(config, *args): + output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID], *args) + await register_light(output_var, config) + return output_var + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_LIGHT") From 0ccc5bf714faa00d7545bd6e09214488df8cb54e Mon Sep 17 00:00:00 2001 From: realzoulou Date: Tue, 13 May 2025 04:05:34 +0200 Subject: [PATCH 070/193] [gps] Add hdop sensor (#8680) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/gps/__init__.py | 13 +++++++++++-- esphome/components/gps/gps.cpp | 19 +++++++++++-------- esphome/components/gps/gps.h | 15 +++++++++------ 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 51288ccc30..88e6f0fd9b 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -25,6 +25,7 @@ GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) GPSListener = gps_ns.class_("GPSListener") CONF_GPS_ID = "gps_id" +CONF_HDOP = "hdop" MULTI_CONF = True CONFIG_SCHEMA = cv.All( cv.Schema( @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, - accuracy_decimals=6, + accuracy_decimals=3, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, @@ -48,12 +49,16 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, - accuracy_decimals=1, + accuracy_decimals=2, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_HDOP): sensor.sensor_schema( + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ), } ) .extend(cv.polling_component_schema("20s")) @@ -92,5 +97,9 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_SATELLITES]) cg.add(var.set_satellites_sensor(sens)) + if hdop_config := config.get(CONF_HDOP): + sens = await sensor.new_sensor(hdop_config) + cg.add(var.set_hdop_sensor(sens)) + # https://platformio.org/lib/show/1655/TinyGPSPlus cg.add_library("mikalhart/TinyGPSPlus", "1.0.2") diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 8c924d629c..e54afdb07e 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -28,6 +28,9 @@ void GPS::update() { if (this->satellites_sensor_ != nullptr) this->satellites_sensor_->publish_state(this->satellites_); + + if (this->hdop_sensor_ != nullptr) + this->hdop_sensor_->publish_state(this->hdop_); } void GPS::loop() { @@ -44,23 +47,23 @@ void GPS::loop() { if (tiny_gps_.speed.isUpdated()) { this->speed_ = tiny_gps_.speed.kmph(); - ESP_LOGD(TAG, "Speed:"); - ESP_LOGD(TAG, " %f km/h", this->speed_); + ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); } if (tiny_gps_.course.isUpdated()) { this->course_ = tiny_gps_.course.deg(); - ESP_LOGD(TAG, "Course:"); - ESP_LOGD(TAG, " %f °", this->course_); + ESP_LOGD(TAG, "Course: %.2f °", this->course_); } if (tiny_gps_.altitude.isUpdated()) { this->altitude_ = tiny_gps_.altitude.meters(); - ESP_LOGD(TAG, "Altitude:"); - ESP_LOGD(TAG, " %f m", this->altitude_); + ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); } if (tiny_gps_.satellites.isUpdated()) { this->satellites_ = tiny_gps_.satellites.value(); - ESP_LOGD(TAG, "Satellites:"); - ESP_LOGD(TAG, " %d", this->satellites_); + ESP_LOGD(TAG, "Satellites: %d", this->satellites_); + } + if (tiny_gps_.hdop.isUpdated()) { + this->hdop_ = tiny_gps_.hdop.hdop(); + ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_); } for (auto *listener : this->listeners_) diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 0626fb0b0e..a400820738 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -33,6 +33,7 @@ class GPS : public PollingComponent, public uart::UARTDevice { void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } + void set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; } void register_listener(GPSListener *listener) { listener->parent_ = this; @@ -46,12 +47,13 @@ class GPS : public PollingComponent, public uart::UARTDevice { TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: - float latitude_ = -1; - float longitude_ = -1; - float speed_ = -1; - float course_ = -1; - float altitude_ = -1; - int satellites_ = -1; + float latitude_ = NAN; + float longitude_ = NAN; + float speed_ = NAN; + float course_ = NAN; + float altitude_ = NAN; + int satellites_ = 0; + double hdop_ = NAN; sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr}; @@ -59,6 +61,7 @@ class GPS : public PollingComponent, public uart::UARTDevice { sensor::Sensor *course_sensor_{nullptr}; sensor::Sensor *altitude_sensor_{nullptr}; sensor::Sensor *satellites_sensor_{nullptr}; + sensor::Sensor *hdop_sensor_{nullptr}; bool has_time_{false}; TinyGPSPlus tiny_gps_; From a83959d738585983279e3dd5d4914b67f1d5671c Mon Sep 17 00:00:00 2001 From: DanielV Date: Tue, 13 May 2025 04:07:54 +0200 Subject: [PATCH 071/193] In case of proto-diff show changes and archive generated (#8698) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index d6469236d5..92d209cc34 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -57,6 +57,17 @@ jobs: event: 'REQUEST_CHANGES', body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.' }) + - if: failure() + name: Show changes + run: git diff + - if: failure() + name: Archive artifacts + uses: actions/upload-artifact@v4.6.2 + with: + name: generated-proto-files + path: | + esphome/components/api/api_pb2.* + esphome/components/api/api_pb2_service.* - if: success() name: Dismiss review uses: actions/github-script@v7.0.1 From 5e164b107a14fa23e9715f1c2066005233f69849 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 16:35:56 +1200 Subject: [PATCH 072/193] [climate] Fix climate_schema (#8772) --- esphome/components/climate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 43c86dcdc2..7007dc13af 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -254,7 +254,7 @@ def climate_schema( icon: str = cv.UNDEFINED, ) -> cv.Schema: schema = { - cv.GenerateID(): cv.declare_id(Climate), + cv.GenerateID(): cv.declare_id(class_), } for key, default, validator in [ From 8b65d1673abc55ba5810f41791152e922de0eb84 Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Mon, 12 May 2025 23:44:51 -0500 Subject: [PATCH 073/193] Tuya Select - Add int_datapoint option (#8393) --- esphome/components/tuya/select/__init__.py | 20 ++++++++++++++----- .../components/tuya/select/tuya_select.cpp | 7 ++++++- esphome/components/tuya/select/tuya_select.h | 6 +++++- esphome/const.py | 1 + tests/components/tuya/common.yaml | 9 ++++++++- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/esphome/components/tuya/select/__init__.py b/esphome/components/tuya/select/__init__.py index a34e279746..e5b2e36ce7 100644 --- a/esphome/components/tuya/select/__init__.py +++ b/esphome/components/tuya/select/__init__.py @@ -1,7 +1,12 @@ import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv -from esphome.const import CONF_ENUM_DATAPOINT, CONF_OPTIMISTIC, CONF_OPTIONS +from esphome.const import ( + CONF_ENUM_DATAPOINT, + CONF_INT_DATAPOINT, + CONF_OPTIMISTIC, + CONF_OPTIONS, +) from .. import CONF_TUYA_ID, Tuya, tuya_ns @@ -26,17 +31,19 @@ def ensure_option_map(value): return value -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( select.select_schema(TuyaSelect) .extend( { cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_ENUM_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_INT_DATAPOINT): cv.uint8_t, cv.Required(CONF_OPTIONS): ensure_option_map, cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, } ) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_ENUM_DATAPOINT, CONF_INT_DATAPOINT), ) @@ -47,5 +54,8 @@ async def to_code(config): cg.add(var.set_select_mappings(list(options_map.keys()))) parent = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(parent)) - cg.add(var.set_select_id(config[CONF_ENUM_DATAPOINT])) + if enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None) is not None: + cg.add(var.set_select_id(enum_datapoint, False)) + if int_datapoint := config.get(CONF_INT_DATAPOINT, None) is not None: + cg.add(var.set_select_id(int_datapoint, True)) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index a4df0873b0..02643e97f4 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -31,7 +31,11 @@ void TuyaSelect::control(const std::string &value) { if (idx.has_value()) { uint8_t mapping = this->mappings_.at(idx.value()); ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str()); - this->parent_->set_enum_datapoint_value(this->select_id_, mapping); + if (this->is_int_) { + this->parent_->set_integer_datapoint_value(this->select_id_, mapping); + } else { + this->parent_->set_enum_datapoint_value(this->select_id_, mapping); + } return; } @@ -41,6 +45,7 @@ void TuyaSelect::control(const std::string &value) { void TuyaSelect::dump_config() { LOG_SELECT("", "Tuya Select", this); ESP_LOGCONFIG(TAG, " Select has datapoint ID %u", this->select_id_); + ESP_LOGCONFIG(TAG, " Data type: %s", this->is_int_ ? "int" : "enum"); ESP_LOGCONFIG(TAG, " Options are:"); auto options = this->traits.get_options(); for (auto i = 0; i < this->mappings_.size(); i++) { diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h index 6a7e5c7ed0..12d7b507d4 100644 --- a/esphome/components/tuya/select/tuya_select.h +++ b/esphome/components/tuya/select/tuya_select.h @@ -16,7 +16,10 @@ class TuyaSelect : public select::Select, public Component { void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_select_id(uint8_t select_id) { this->select_id_ = select_id; } + void set_select_id(uint8_t select_id, bool is_int) { + this->select_id_ = select_id; + this->is_int_ = is_int; + } void set_select_mappings(std::vector mappings) { this->mappings_ = std::move(mappings); } protected: @@ -26,6 +29,7 @@ class TuyaSelect : public select::Select, public Component { bool optimistic_ = false; uint8_t select_id_; std::vector mappings_; + bool is_int_ = false; }; } // namespace tuya diff --git a/esphome/const.py b/esphome/const.py index d656b15519..0f811aa870 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -408,6 +408,7 @@ CONF_INITIAL_OPTION = "initial_option" CONF_INITIAL_STATE = "initial_state" CONF_INITIAL_VALUE = "initial_value" CONF_INPUT = "input" +CONF_INT_DATAPOINT = "int_datapoint" CONF_INTEGRATION_TIME = "integration_time" CONF_INTENSITY = "intensity" CONF_INTERLOCK = "interlock" diff --git a/tests/components/tuya/common.yaml b/tests/components/tuya/common.yaml index fcf8a2d96b..2c40628139 100644 --- a/tests/components/tuya/common.yaml +++ b/tests/components/tuya/common.yaml @@ -60,12 +60,19 @@ number: select: - platform: tuya - id: tuya_select + id: tuya_select_enum enum_datapoint: 42 options: 0: Internal 1: Floor 2: Both + - platform: tuya + id: tuya_select_int + int_datapoint: 43 + options: + 0: Internal + 1: Floor + 2: Both sensor: - platform: tuya From 0cf9b05afd7ebb1b680eff71bb5107da87601a7b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 17:07:57 +1200 Subject: [PATCH 074/193] [select] Tidy schema generation (#8775) --- esphome/components/select/__init__.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 2b31ae36d8..ecbba8677b 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -66,29 +66,25 @@ _SELECT_SCHEMA = ( def select_schema( - class_: MockObjClass = cv.UNDEFINED, + class_: MockObjClass, *, entity_category: str = cv.UNDEFINED, icon: str = cv.UNDEFINED, ): - schema = cv.Schema({}) - if class_ is not cv.UNDEFINED: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if entity_category is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - if icon is not cv.UNDEFINED: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + schema = {cv.GenerateID(): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + return _SELECT_SCHEMA.extend(schema) # Remove before 2025.11.0 -SELECT_SCHEMA = select_schema() +SELECT_SCHEMA = select_schema(Select) SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) From 2560d2b9d09e93e262113ba0f9d575db6db2c6fd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 17:16:23 +1200 Subject: [PATCH 075/193] [demo] Clean up schema deprecations, add test (#8771) --- esphome/components/demo/__init__.py | 38 +++++++++++------------ tests/components/demo/test.esp32-idf.yaml | 1 + 2 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 tests/components/demo/test.esp32-idf.yaml diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 349bd8e4cb..96ffb58b82 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -17,7 +17,6 @@ from esphome.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_ICON, - CONF_ID, CONF_INVERTED, CONF_MAX_VALUE, CONF_MIN_VALUE, @@ -153,9 +152,10 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + climate.climate_schema(DemoClimate) + .extend(cv.COMPONENT_SCHEMA) + .extend( { - cv.GenerateID(): cv.declare_id(DemoClimate), cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True), } ) @@ -183,9 +183,10 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + cover.cover_schema(DemoCover) + .extend(cv.COMPONENT_SCHEMA) + .extend( { - cv.GenerateID(): cv.declare_id(DemoCover), cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True), } ) @@ -211,9 +212,10 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + fan.fan_schema(DemoFan) + .extend(cv.COMPONENT_SCHEMA) + .extend( { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan), cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True), } ) @@ -251,7 +253,9 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + light.light_schema(DemoLight, light.LightType.RGB) + .extend(cv.COMPONENT_SCHEMA) + .extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight), cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True), @@ -377,39 +381,33 @@ async def to_code(config): await cg.register_component(var, conf) for conf in config[CONF_CLIMATES]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await climate.new_climate(conf) await cg.register_component(var, conf) - await climate.register_climate(var, conf) cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_COVERS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await cover.new_cover(conf) await cg.register_component(var, conf) - await cover.register_cover(var, conf) cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_FANS]: - var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + var = await fan.new_fan(conf) await cg.register_component(var, conf) - await fan.register_fan(var, conf) cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_LIGHTS]: - var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + var = await light.new_light(conf) await cg.register_component(var, conf) - await light.register_light(var, conf) cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_NUMBERS]: - var = cg.new_Pvariable(conf[CONF_ID]) - await cg.register_component(var, conf) - await number.register_number( - var, + var = await number.new_number( conf, min_value=conf[CONF_MIN_VALUE], max_value=conf[CONF_MAX_VALUE], step=conf[CONF_STEP], ) + await cg.register_component(var, conf) cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_SENSORS]: diff --git a/tests/components/demo/test.esp32-idf.yaml b/tests/components/demo/test.esp32-idf.yaml new file mode 100644 index 0000000000..80027786df --- /dev/null +++ b/tests/components/demo/test.esp32-idf.yaml @@ -0,0 +1 @@ +demo: From 864ae7a56ccd15874a46f7d345f14d4927cc72b3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 17:26:07 +1200 Subject: [PATCH 076/193] [template] Use alarm_control_panel_schema method (#8764) --- .../template/alarm_control_panel/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 8b13bcd29f..a406c626ee 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import alarm_control_panel, binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_BINARY_SENSORS, CONF_ID, CONF_INPUT, CONF_RESTORE_MODE +from esphome.const import CONF_BINARY_SENSORS, CONF_INPUT, CONF_RESTORE_MODE from .. import template_ns @@ -77,9 +77,9 @@ TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value( ) TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA = ( - alarm_control_panel.ALARM_CONTROL_PANEL_SCHEMA.extend( + alarm_control_panel.alarm_control_panel_schema(TemplateAlarmControlPanel) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateAlarmControlPanel), cv.Optional(CONF_CODES): cv.ensure_list(cv.string_strict), cv.Optional(CONF_REQUIRES_CODE_TO_ARM): cv.boolean, cv.Optional(CONF_ARMING_HOME_TIME): cv.positive_time_period_milliseconds, @@ -100,7 +100,8 @@ TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA = ( RESTORE_MODES, upper=True ), } - ).extend(cv.COMPONENT_SCHEMA) + ) + .extend(cv.COMPONENT_SCHEMA) ) CONFIG_SCHEMA = cv.All( @@ -110,9 +111,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await alarm_control_panel.new_alarm_control_panel(config) await cg.register_component(var, config) - await alarm_control_panel.register_alarm_control_panel(var, config) if CONF_CODES in config: for acode in config[CONF_CODES]: cg.add(var.add_code(acode)) From a36e1aab8eb0ad1aaee4b9a4c6c55dd0a2aad730 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 17:29:00 +1200 Subject: [PATCH 077/193] [cover] Update components to use ``cover_schema(...)`` (#8770) --- esphome/components/am43/cover/__init__.py | 9 +-- esphome/components/copy/cover/__init__.py | 19 ++--- esphome/components/current_based/cover.py | 73 ++++++++++--------- esphome/components/endstop/cover.py | 33 +++++---- esphome/components/feedback/cover.py | 71 +++++++++--------- esphome/components/he60r/cover.py | 9 +-- esphome/components/template/cover/__init__.py | 48 ++++++------ esphome/components/time_based/cover.py | 33 +++++---- esphome/components/tormatic/cover.py | 9 +-- esphome/components/tuya/cover/__init__.py | 17 ++--- 10 files changed, 164 insertions(+), 157 deletions(-) diff --git a/esphome/components/am43/cover/__init__.py b/esphome/components/am43/cover/__init__.py index d60f9cd4e7..e4ecf1444f 100644 --- a/esphome/components/am43/cover/__init__.py +++ b/esphome/components/am43/cover/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import ble_client, cover import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_PIN +from esphome.const import CONF_PIN CODEOWNERS = ["@buxtronix"] DEPENDENCIES = ["ble_client"] @@ -15,9 +15,9 @@ Am43Component = am43_ns.class_( ) CONFIG_SCHEMA = ( - cover.COVER_SCHEMA.extend( + cover.cover_schema(Am43Component) + .extend( { - cv.GenerateID(): cv.declare_id(Am43Component), cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF), cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, } @@ -28,9 +28,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) cg.add(var.set_pin(config[CONF_PIN])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) await cg.register_component(var, config) - await cover.register_cover(var, config) await ble_client.register_ble_node(var, config) diff --git a/esphome/components/copy/cover/__init__.py b/esphome/components/copy/cover/__init__.py index 7db9034d02..ff5bef5668 100644 --- a/esphome/components/copy/cover/__init__.py +++ b/esphome/components/copy/cover/__init__.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_ICON, - CONF_ID, CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from @@ -15,12 +14,15 @@ from .. import copy_ns CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component) -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyCover), - cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cover.cover_schema(CopyCover) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), @@ -30,8 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cover.register_cover(var, config) + var = await cover.new_cover(config) await cg.register_component(var, config) source = await cg.get_variable(config[CONF_SOURCE_ID]) diff --git a/esphome/components/current_based/cover.py b/esphome/components/current_based/cover.py index 75f083ef14..99952adb12 100644 --- a/esphome/components/current_based/cover.py +++ b/esphome/components/current_based/cover.py @@ -5,7 +5,6 @@ import esphome.config_validation as cv from esphome.const import ( CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, - CONF_ID, CONF_MAX_DURATION, CONF_OPEN_ACTION, CONF_OPEN_DURATION, @@ -30,45 +29,47 @@ CurrentBasedCover = current_based_ns.class_( "CurrentBasedCover", cover.Cover, cg.Component ) -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CurrentBasedCover), - cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range( - min=0, min_included=False - ), - cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( - min=0, min_included=False - ), - cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, - cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range( - min=0, min_included=False - ), - cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( - min=0, min_included=False - ), - cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, - cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean, - cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation( - single=True - ), - cv.Optional( - CONF_START_SENSING_DELAY, default="500ms" - ): cv.positive_time_period_milliseconds, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cover.cover_schema(CurrentBasedCover) + .extend( + { + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean, + cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_START_SENSING_DELAY, default="500ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) await automation.build_automation( var.get_stop_trigger(), [], config[CONF_STOP_ACTION] diff --git a/esphome/components/endstop/cover.py b/esphome/components/endstop/cover.py index 286c876ff6..c16680b6af 100644 --- a/esphome/components/endstop/cover.py +++ b/esphome/components/endstop/cover.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_CLOSE_ENDSTOP, - CONF_ID, CONF_MAX_DURATION, CONF_OPEN_ACTION, CONF_OPEN_DURATION, @@ -17,25 +16,27 @@ from esphome.const import ( endstop_ns = cg.esphome_ns.namespace("endstop") EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component) -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EndstopCover), - cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), - cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, - cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), - cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cover.cover_schema(EndstopCover) + .extend( + { + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) await automation.build_automation( var.get_stop_trigger(), [], config[CONF_STOP_ACTION] diff --git a/esphome/components/feedback/cover.py b/esphome/components/feedback/cover.py index b90374f6e8..856818280f 100644 --- a/esphome/components/feedback/cover.py +++ b/esphome/components/feedback/cover.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_CLOSE_ENDSTOP, - CONF_ID, CONF_MAX_DURATION, CONF_OPEN_ACTION, CONF_OPEN_DURATION, @@ -50,36 +49,43 @@ def validate_infer_endstop(config): return config -CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(FeedbackCover), - cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), - cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor), - cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), - cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), - cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor), - cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), - cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE): cv.boolean, - cv.Optional( - CONF_UPDATE_INTERVAL, "1000ms" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean, - cv.Optional( - CONF_DIRECTION_CHANGE_WAIT_TIME - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_ACCELERATION_WAIT_TIME, "0s" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, - }, -).extend(cv.COMPONENT_SCHEMA) +CONFIG_FEEDBACK_COVER_BASE_SCHEMA = ( + cover.cover_schema(FeedbackCover) + .extend( + { + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id( + binary_sensor.BinarySensor + ), + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id( + binary_sensor.BinarySensor + ), + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE): cv.boolean, + cv.Optional( + CONF_UPDATE_INTERVAL, "1000ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean, + cv.Optional( + CONF_DIRECTION_CHANGE_WAIT_TIME + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_ACCELERATION_WAIT_TIME, "0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, + }, + ) + .extend(cv.COMPONENT_SCHEMA) +) CONFIG_SCHEMA = cv.All( @@ -90,9 +96,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) # STOP await automation.build_automation( diff --git a/esphome/components/he60r/cover.py b/esphome/components/he60r/cover.py index a483d2a571..a3a1b19f5a 100644 --- a/esphome/components/he60r/cover.py +++ b/esphome/components/he60r/cover.py @@ -1,17 +1,17 @@ import esphome.codegen as cg from esphome.components import cover, uart import esphome.config_validation as cv -from esphome.const import CONF_CLOSE_DURATION, CONF_ID, CONF_OPEN_DURATION +from esphome.const import CONF_CLOSE_DURATION, CONF_OPEN_DURATION he60r_ns = cg.esphome_ns.namespace("he60r") HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component) CONFIG_SCHEMA = ( - cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA) + cover.cover_schema(HE60rCover) + .extend(uart.UART_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA) .extend( { - cv.GenerateID(): cv.declare_id(HE60rCover), cv.Optional( CONF_OPEN_DURATION, default="15s" ): cv.positive_time_period_milliseconds, @@ -34,9 +34,8 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) await uart.register_uart_device(var, config) cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 5129e6b1af..a4fb0b7021 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -34,31 +34,37 @@ RESTORE_MODES = { CONF_HAS_POSITION = "has_position" CONF_TOGGLE_ACTION = "toggle_action" -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateCover), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, - cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, - cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( - RESTORE_MODES, upper=True - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cover.cover_schema(TemplateCover) + .extend( + { + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, + cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) if CONF_LAMBDA in config: template_ = await cg.process_lambda( config[CONF_LAMBDA], [], return_type=cg.optional.template(float) diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index c723345370..d14332d453 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, - CONF_ID, CONF_OPEN_ACTION, CONF_OPEN_DURATION, CONF_STOP_ACTION, @@ -18,25 +17,27 @@ TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Componen CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" CONF_MANUAL_CONTROL = "manual_control" -CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TimeBasedCover), - cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, - cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), - cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, - cv.Optional(CONF_MANUAL_CONTROL, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cover.cover_schema(TimeBasedCover) + .extend( + { + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_MANUAL_CONTROL, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) await automation.build_automation( var.get_stop_trigger(), [], config[CONF_STOP_ACTION] diff --git a/esphome/components/tormatic/cover.py b/esphome/components/tormatic/cover.py index 627ae6b63d..447920326b 100644 --- a/esphome/components/tormatic/cover.py +++ b/esphome/components/tormatic/cover.py @@ -1,17 +1,17 @@ import esphome.codegen as cg from esphome.components import cover, uart import esphome.config_validation as cv -from esphome.const import CONF_CLOSE_DURATION, CONF_ID, CONF_OPEN_DURATION +from esphome.const import CONF_CLOSE_DURATION, CONF_OPEN_DURATION tormatic_ns = cg.esphome_ns.namespace("tormatic") Tormatic = tormatic_ns.class_("Tormatic", cover.Cover, cg.PollingComponent) CONFIG_SCHEMA = ( - cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA) + cover.cover_schema(Tormatic) + .extend(uart.UART_DEVICE_SCHEMA) .extend(cv.polling_component_schema("300ms")) .extend( { - cv.GenerateID(): cv.declare_id(Tormatic), cv.Optional( CONF_OPEN_DURATION, default="15s" ): cv.positive_time_period_milliseconds, @@ -34,9 +34,8 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) await uart.register_uart_device(var, config) cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) diff --git a/esphome/components/tuya/cover/__init__.py b/esphome/components/tuya/cover/__init__.py index 61029b6daa..8c610c0272 100644 --- a/esphome/components/tuya/cover/__init__.py +++ b/esphome/components/tuya/cover/__init__.py @@ -1,12 +1,7 @@ import esphome.codegen as cg from esphome.components import cover import esphome.config_validation as cv -from esphome.const import ( - CONF_MAX_VALUE, - CONF_MIN_VALUE, - CONF_OUTPUT_ID, - CONF_RESTORE_MODE, -) +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_RESTORE_MODE from .. import CONF_TUYA_ID, Tuya, tuya_ns @@ -38,9 +33,9 @@ def validate_range(config): CONFIG_SCHEMA = cv.All( - cover.COVER_SCHEMA.extend( + cover.cover_schema(TuyaCover) + .extend( { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaCover), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_CONTROL_DATAPOINT): cv.uint8_t, cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, @@ -54,15 +49,15 @@ CONFIG_SCHEMA = cv.All( RESTORE_MODES, upper=True ), }, - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_range, ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await cover.new_cover(config) await cg.register_component(var, config) - await cover.register_cover(var, config) if CONF_CONTROL_DATAPOINT in config: cg.add(var.set_control_id(config[CONF_CONTROL_DATAPOINT])) From 410b6353fefbfe1e9e093a7bd2a6111260d985bf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 18:17:54 +1200 Subject: [PATCH 078/193] [switch] Fix schema generation (#8774) --- .../dfrobot_sen0395/switch/__init__.py | 43 ++++++------ esphome/components/switch/__init__.py | 70 ++++++++----------- tests/components/dfrobot_sen0395/common.yaml | 14 ++++ 3 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py index f854d08398..8e492080de 100644 --- a/esphome/components/dfrobot_sen0395/switch/__init__.py +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG +from esphome.cpp_generator import MockObjClass from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component @@ -26,32 +27,30 @@ Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_( "Sen0395StartAfterBootSwitch", DfrobotSen0395Switch ) -_SWITCH_SCHEMA = ( - switch.switch_schema( - entity_category=ENTITY_CATEGORY_CONFIG, + +def _switch_schema(class_: MockObjClass) -> cv.Schema: + return ( + switch.switch_schema( + class_, + entity_category=ENTITY_CATEGORY_CONFIG, + ) + .extend( + { + cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id( + DfrobotSen0395Component + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) - .extend( - { - cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), - } - ) - .extend(cv.COMPONENT_SCHEMA) -) + CONFIG_SCHEMA = cv.typed_schema( { - "sensor_active": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)} - ), - "turn_on_led": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)} - ), - "presence_via_uart": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)} - ), - "start_after_boot": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)} - ), + "sensor_active": _switch_schema(Sen0395PowerSwitch), + "turn_on_led": _switch_schema(Sen0395LedSwitch), + "presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch), + "start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch), } ) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 1c65aa8dfc..e7445051e0 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -72,6 +72,9 @@ _SWITCH_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), cv.Optional(CONF_INVERTED): cv.boolean, + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), @@ -89,54 +92,41 @@ _SWITCH_SCHEMA = ( def switch_schema( - class_: MockObjClass = cv.UNDEFINED, + class_: MockObjClass, *, - entity_category: str = cv.UNDEFINED, - device_class: str = cv.UNDEFINED, - icon: str = cv.UNDEFINED, block_inverted: bool = False, - default_restore_mode: str = "ALWAYS_OFF", + default_restore_mode: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, ): - schema = _SWITCH_SCHEMA.extend( - { - cv.Optional(CONF_RESTORE_MODE, default=default_restore_mode): cv.enum( - RESTORE_MODES, upper=True, space="_" - ), - } - ) - if class_ is not cv.UNDEFINED: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if entity_category is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - if device_class is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if icon is not cv.UNDEFINED: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + schema = {cv.GenerateID(): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ( + CONF_RESTORE_MODE, + default_restore_mode, + cv.enum(RESTORE_MODES, upper=True, space="_") + if default_restore_mode is not cv.UNDEFINED + else cv.UNDEFINED, + ), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + if block_inverted: - schema = schema.extend( - { - cv.Optional(CONF_INVERTED): cv.invalid( - "Inverted is not supported for this platform!" - ) - } + schema[cv.Optional(CONF_INVERTED)] = cv.invalid( + "Inverted is not supported for this platform!" ) - return schema + + return _SWITCH_SCHEMA.extend(schema) # Remove before 2025.11.0 -SWITCH_SCHEMA = switch_schema() +SWITCH_SCHEMA = switch_schema(Switch) SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) diff --git a/tests/components/dfrobot_sen0395/common.yaml b/tests/components/dfrobot_sen0395/common.yaml index 69bcebf182..8c349911d3 100644 --- a/tests/components/dfrobot_sen0395/common.yaml +++ b/tests/components/dfrobot_sen0395/common.yaml @@ -26,3 +26,17 @@ dfrobot_sen0395: binary_sensor: - platform: dfrobot_sen0395 id: mmwave_detected + +switch: + - platform: dfrobot_sen0395 + type: sensor_active + id: mmwave_sensor_active + - platform: dfrobot_sen0395 + type: turn_on_led + id: mmwave_turn_on_led + - platform: dfrobot_sen0395 + type: presence_via_uart + id: mmwave_presence_via_uart + - platform: dfrobot_sen0395 + type: start_after_boot + id: mmwave_start_after_boot From c5654b4cb2d96ae8848d100939caff9bdb5d80d9 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 13 May 2025 08:24:38 +0200 Subject: [PATCH 079/193] [esp32] improve `gpio` (#8709) --- esphome/components/esp32/gpio.py | 31 +++++++++++------------ esphome/components/esp32/gpio_esp32.py | 3 +-- esphome/components/esp32/gpio_esp32_c2.py | 2 +- esphome/components/esp32/gpio_esp32_c3.py | 2 +- esphome/components/esp32/gpio_esp32_c6.py | 2 +- esphome/components/esp32/gpio_esp32_h2.py | 2 +- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index df01769a66..2bb10ce6ec 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,6 +1,6 @@ from dataclasses import dataclass import logging -from typing import Any +from typing import Any, Callable from esphome import pins import esphome.codegen as cg @@ -64,8 +64,7 @@ def _lookup_pin(value): def _translate_pin(value): if isinstance(value, dict) or value is None: raise cv.Invalid( - "This variable only supports pin numbers, not full pin schemas " - "(with inverted and mode)." + "This variable only supports pin numbers, not full pin schemas (with inverted and mode)." ) if isinstance(value, int) and not isinstance(value, bool): return value @@ -82,30 +81,22 @@ def _translate_pin(value): @dataclass class ESP32ValidationFunctions: - pin_validation: Any - usage_validation: Any + pin_validation: Callable[[Any], Any] + usage_validation: Callable[[Any], Any] _esp32_validations = { VARIANT_ESP32: ESP32ValidationFunctions( pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports ), - VARIANT_ESP32S2: ESP32ValidationFunctions( - pin_validation=esp32_s2_validate_gpio_pin, - usage_validation=esp32_s2_validate_supports, + VARIANT_ESP32C2: ESP32ValidationFunctions( + pin_validation=esp32_c2_validate_gpio_pin, + usage_validation=esp32_c2_validate_supports, ), VARIANT_ESP32C3: ESP32ValidationFunctions( pin_validation=esp32_c3_validate_gpio_pin, usage_validation=esp32_c3_validate_supports, ), - VARIANT_ESP32S3: ESP32ValidationFunctions( - pin_validation=esp32_s3_validate_gpio_pin, - usage_validation=esp32_s3_validate_supports, - ), - VARIANT_ESP32C2: ESP32ValidationFunctions( - pin_validation=esp32_c2_validate_gpio_pin, - usage_validation=esp32_c2_validate_supports, - ), VARIANT_ESP32C6: ESP32ValidationFunctions( pin_validation=esp32_c6_validate_gpio_pin, usage_validation=esp32_c6_validate_supports, @@ -114,6 +105,14 @@ _esp32_validations = { pin_validation=esp32_h2_validate_gpio_pin, usage_validation=esp32_h2_validate_supports, ), + VARIANT_ESP32S2: ESP32ValidationFunctions( + pin_validation=esp32_s2_validate_gpio_pin, + usage_validation=esp32_s2_validate_supports, + ), + VARIANT_ESP32S3: ESP32ValidationFunctions( + pin_validation=esp32_s3_validate_gpio_pin, + usage_validation=esp32_s3_validate_supports, + ), } diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index e4d3b6aaf3..973d2dc0ef 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -31,8 +31,7 @@ def esp32_validate_gpio_pin(value): ) if 9 <= value <= 10: _LOGGER.warning( - "Pin %s (9-10) might already be used by the " - "flash interface in QUAD IO flash mode.", + "Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.", value, ) if value in (24, 28, 29, 30, 31): diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py index abdcb1b655..32a24050ca 100644 --- a/esphome/components/esp32/gpio_esp32_c2.py +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -22,7 +22,7 @@ def esp32_c2_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 20: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-20)") if is_input: # All ESP32 pins support input mode diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 5b9ec0ebd9..c1427cc02a 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -35,7 +35,7 @@ def esp32_c3_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 21: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-21)") if is_input: # All ESP32 pins support input mode diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index bc735f85c4..d466adb994 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -36,7 +36,7 @@ def esp32_c6_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 23: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-23)") if is_input: # All ESP32 pins support input mode pass diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 7413bf4db5..7c3a658b17 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -45,7 +45,7 @@ def esp32_h2_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 27: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-27)") if is_input: # All ESP32 pins support input mode pass From 6f8ee659196ec4493e17a091bc205814c4025eed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 18:34:26 +1200 Subject: [PATCH 080/193] [text_sensor] Fix schema generation (#8773) --- esphome/components/text_sensor/__init__.py | 38 +++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 92b08aa6d0..888b65745f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -156,32 +156,24 @@ _TEXT_SENSOR_SCHEMA = ( def text_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, - icon: str = cv.UNDEFINED, - entity_category: str = cv.UNDEFINED, device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, ) -> cv.Schema: - schema = _TEXT_SENSOR_SCHEMA + schema = {} + if class_ is not cv.UNDEFINED: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if icon is not cv.UNDEFINED: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - if device_class is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if entity_category is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - return schema + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _TEXT_SENSOR_SCHEMA.extend(schema) # Remove before 2025.11.0 From 032949bc779880241421b8ee5c56af16adc81ddf Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Tue, 13 May 2025 14:35:19 +0200 Subject: [PATCH 081/193] [audio] Fix: Decoder stops unnecessarily after a potential failure is detected. (#8776) --- esphome/components/audio/audio_decoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 60489d7d78..c74b028c4b 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -171,7 +171,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { bytes_available_before_processing = this->input_transfer_buffer_->available(); - if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) { + if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) { // Failed to decode in last attempt and there is no new data if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { From 0aa7911b1be979dc7c80fa855d484bd66366d0ca Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 13 May 2025 13:58:15 -0700 Subject: [PATCH 082/193] [esp32][esp8266] use low-level pin control for ISR gpio (#8743) Co-authored-by: Samuel Sieb --- esphome/components/esp32/gpio.cpp | 110 ++++++++++++++++++++++------ esphome/components/esp8266/gpio.cpp | 77 +++++++++++++++++-- 2 files changed, 155 insertions(+), 32 deletions(-) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 7896597d3e..b554b6d09c 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -2,42 +2,66 @@ #include "gpio.h" #include "esphome/core/log.h" +#include "driver/gpio.h" +#include "driver/rtc_io.h" +#include "hal/gpio_hal.h" +#include "soc/soc_caps.h" +#include "soc/gpio_periph.h" #include +#if (SOC_RTCIO_PIN_COUNT > 0) +#include "hal/rtc_io_hal.h" +#endif + +#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT +#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT +#endif + namespace esphome { namespace esp32 { static const char *const TAG = "esp32"; +static const gpio_hal_context_t GPIO_HAL = {.dev = GPIO_HAL_GET_HW(GPIO_PORT_0)}; + bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { +static gpio_mode_t flags_to_mode(gpio::Flags flags) { flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); - if (flags == gpio::FLAG_INPUT) { + if (flags == gpio::FLAG_INPUT) return GPIO_MODE_INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { + if (flags == gpio::FLAG_OUTPUT) return GPIO_MODE_OUTPUT; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) return GPIO_MODE_OUTPUT_OD; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) return GPIO_MODE_INPUT_OUTPUT_OD; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) { + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) return GPIO_MODE_INPUT_OUTPUT; - } else { - // unsupported or gpio::FLAG_NONE - return GPIO_MODE_DISABLE; - } + // unsupported or gpio::FLAG_NONE + return GPIO_MODE_DISABLE; } struct ISRPinArg { gpio_num_t pin; + gpio::Flags flags; bool inverted; +#if defined(USE_ESP32_VARIANT_ESP32) + bool use_rtc; + int rtc_pin; +#endif }; ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; + arg->pin = this->pin_; + arg->flags = gpio::FLAG_NONE; arg->inverted = inverted_; +#if defined(USE_ESP32_VARIANT_ESP32) + arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_); + if (arg->use_rtc) + arg->rtc_pin = rtc_io_number_get(this->pin_); +#endif return ISRInternalGPIOPin((void *) arg); } @@ -90,6 +114,7 @@ void ESP32InternalGPIOPin::setup() { if (flags_ & gpio::FLAG_OUTPUT) { gpio_set_drive_capability(pin_, drive_strength_); } + ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT); } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { @@ -115,28 +140,65 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } using namespace esp32; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { - auto *arg = reinterpret_cast(arg_); - return bool(gpio_get_level(arg->pin)) != arg->inverted; + auto *arg = reinterpret_cast(this->arg_); + return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted; } + void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { - auto *arg = reinterpret_cast(arg_); - gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0); + auto *arg = reinterpret_cast(this->arg_); + gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted); } + void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { // not supported } + void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { auto *arg = reinterpret_cast(arg_); - gpio_set_direction(arg->pin, flags_to_mode(flags)); - gpio_pull_mode_t pull_mode = GPIO_FLOATING; - if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { - pull_mode = GPIO_PULLUP_PULLDOWN; - } else if (flags & gpio::FLAG_PULLUP) { - pull_mode = GPIO_PULLUP_ONLY; - } else if (flags & gpio::FLAG_PULLDOWN) { - pull_mode = GPIO_PULLDOWN_ONLY; + gpio::Flags diff = (gpio::Flags)(flags ^ arg->flags); + if (diff & gpio::FLAG_OUTPUT) { + if (flags & gpio::FLAG_OUTPUT) { + gpio_hal_output_enable(&GPIO_HAL, arg->pin); + if (flags & gpio::FLAG_OPEN_DRAIN) + gpio_hal_od_enable(&GPIO_HAL, arg->pin); + } else { + gpio_hal_output_disable(&GPIO_HAL, arg->pin); + } } - gpio_set_pull_mode(arg->pin, pull_mode); + if (diff & gpio::FLAG_INPUT) { + if (flags & gpio::FLAG_INPUT) { + gpio_hal_input_enable(&GPIO_HAL, arg->pin); +#if defined(USE_ESP32_VARIANT_ESP32) + if (arg->use_rtc) { + if (flags & gpio::FLAG_PULLUP) { + rtcio_hal_pullup_enable(arg->rtc_pin); + } else { + rtcio_hal_pullup_disable(arg->rtc_pin); + } + if (flags & gpio::FLAG_PULLDOWN) { + rtcio_hal_pulldown_enable(arg->rtc_pin); + } else { + rtcio_hal_pulldown_disable(arg->rtc_pin); + } + } else +#endif + { + if (flags & gpio::FLAG_PULLUP) { + gpio_hal_pullup_en(&GPIO_HAL, arg->pin); + } else { + gpio_hal_pullup_dis(&GPIO_HAL, arg->pin); + } + if (flags & gpio::FLAG_PULLDOWN) { + gpio_hal_pulldown_en(&GPIO_HAL, arg->pin); + } else { + gpio_hal_pulldown_dis(&GPIO_HAL, arg->pin); + } + } + } else { + gpio_hal_input_disable(&GPIO_HAL, arg->pin); + } + } + arg->flags = flags; } } // namespace esphome diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index a24f217756..9f23e8e67e 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -8,7 +8,7 @@ namespace esp8266 { static const char *const TAG = "esp8266"; -static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { +static int flags_to_mode(gpio::Flags flags, uint8_t pin) { if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) return INPUT; } else if (flags == gpio::FLAG_OUTPUT) { @@ -34,12 +34,36 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { struct ISRPinArg { uint8_t pin; bool inverted; + volatile uint32_t *in_reg; + volatile uint32_t *out_set_reg; + volatile uint32_t *out_clr_reg; + volatile uint32_t *mode_set_reg; + volatile uint32_t *mode_clr_reg; + volatile uint32_t *func_reg; + uint32_t mask; }; ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; - arg->inverted = inverted_; + arg->pin = this->pin_; + arg->inverted = this->inverted_; + if (this->pin_ < 16) { + arg->in_reg = &GPI; + arg->out_set_reg = &GPOS; + arg->out_clr_reg = &GPOC; + arg->mode_set_reg = &GPES; + arg->mode_clr_reg = &GPEC; + arg->func_reg = &GPF(this->pin_); + arg->mask = 1 << this->pin_; + } else { + arg->in_reg = &GP16I; + arg->out_set_reg = &GP16O; + arg->out_clr_reg = nullptr; + arg->mode_set_reg = &GP16E; + arg->mode_clr_reg = nullptr; + arg->func_reg = &GPF16; + arg->mask = 1; + } return ISRInternalGPIOPin((void *) arg); } @@ -88,20 +112,57 @@ void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } using namespace esp8266; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { - auto *arg = reinterpret_cast(arg_); - return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT + auto *arg = reinterpret_cast(this->arg_); + return bool(*arg->in_reg & arg->mask) != arg->inverted; } + void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { auto *arg = reinterpret_cast(arg_); - digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT + if (arg->pin < 16) { + if (value != arg->inverted) { + *arg->out_set_reg = arg->mask; + } else { + *arg->out_clr_reg = arg->mask; + } + } else { + if (value != arg->inverted) { + *arg->out_set_reg |= 1; + } else { + *arg->out_set_reg &= ~1; + } + } } + void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { auto *arg = reinterpret_cast(arg_); GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); } + void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { - auto *arg = reinterpret_cast(arg_); - pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT + auto *arg = reinterpret_cast(this->arg_); + if (arg->pin < 16) { + if (flags & gpio::FLAG_OUTPUT) { + *arg->mode_set_reg = arg->mask; + } else { + *arg->mode_clr_reg = arg->mask; + } + if (flags & gpio::FLAG_PULLUP) { + *arg->func_reg |= 1 << GPFPU; + } else { + *arg->func_reg &= ~(1 << GPFPU); + } + } else { + if (flags & gpio::FLAG_OUTPUT) { + *arg->mode_set_reg |= 1; + } else { + *arg->mode_set_reg &= ~1; + } + if (flags & gpio::FLAG_PULLDOWN) { + *arg->func_reg |= 1 << GP16FPD; + } else { + *arg->func_reg &= ~(1 << GP16FPD); + } + } } } // namespace esphome From 4ea63af7966d8e275c93fabb07ed6e0f581095a3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 09:21:19 +1200 Subject: [PATCH 083/193] [online_image] Support 24 bit bmp images (#8612) --- esphome/components/online_image/bmp_image.cpp | 59 +++++++++++++++---- esphome/components/online_image/bmp_image.h | 2 + 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp index af9019a4d2..f55c9f1813 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/online_image/bmp_image.cpp @@ -62,6 +62,13 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { case 1: this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); break; + case 24: + this->width_bytes_ = this->width_ * 3; + if (this->width_bytes_ % 4 != 0) { + this->padding_bytes_ = 4 - (this->width_bytes_ % 4); + this->width_bytes_ += this->padding_bytes_; + } + break; default: ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_); return DECODE_ERROR_UNSUPPORTED_FORMAT; @@ -78,18 +85,48 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->current_index_ = this->data_offset_; index = this->data_offset_; } - while (index < size) { - size_t paint_index = this->current_index_ - this->data_offset_; - - uint8_t current_byte = buffer[index]; - for (uint8_t i = 0; i < 8; i++) { - size_t x = (paint_index * 8) % this->width_ + i; - size_t y = (this->height_ - 1) - (paint_index / this->width_bytes_); - Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; - this->draw(x, y, 1, 1, c); + switch (this->bits_per_pixel_) { + case 1: { + while (index < size) { + uint8_t current_byte = buffer[index]; + for (uint8_t i = 0; i < 8; i++) { + size_t x = (this->paint_index_ % this->width_) + i; + size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; + this->draw(x, y, 1, 1, c); + } + this->paint_index_ += 8; + this->current_index_++; + index++; + } + break; } - this->current_index_++; - index++; + case 24: { + while (index < size) { + if (index + 2 >= size) { + this->decoded_bytes_ += index; + return index; + } + uint8_t b = buffer[index]; + uint8_t g = buffer[index + 1]; + uint8_t r = buffer[index + 2]; + size_t x = this->paint_index_ % this->width_; + size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + Color c = Color(r, g, b); + this->draw(x, y, 1, 1, c); + this->paint_index_++; + this->current_index_ += 3; + index += 3; + if (x == this->width_ - 1 && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } + } + break; + } + default: + ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; } this->decoded_bytes_ += size; return size; diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/online_image/bmp_image.h index 61192f6a46..916ffea1ad 100644 --- a/esphome/components/online_image/bmp_image.h +++ b/esphome/components/online_image/bmp_image.h @@ -24,6 +24,7 @@ class BmpDecoder : public ImageDecoder { protected: size_t current_index_{0}; + size_t paint_index_{0}; ssize_t width_{0}; ssize_t height_{0}; uint16_t bits_per_pixel_{0}; @@ -32,6 +33,7 @@ class BmpDecoder : public ImageDecoder { uint32_t color_table_entries_{0}; size_t width_bytes_{0}; size_t data_offset_{0}; + uint8_t padding_bytes_{0}; }; } // namespace online_image From 183659f52766c94a5692266e892bf7680d03f93e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 14 May 2025 07:22:58 +1000 Subject: [PATCH 084/193] [mipi_spi] New display driver for MIPI DBI devices (#8383) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/mipi_spi/__init__.py | 15 + esphome/components/mipi_spi/display.py | 474 +++++++++++ esphome/components/mipi_spi/mipi_spi.cpp | 481 +++++++++++ esphome/components/mipi_spi/mipi_spi.h | 171 ++++ .../components/mipi_spi/models/__init__.py | 65 ++ esphome/components/mipi_spi/models/amoled.py | 72 ++ .../components/mipi_spi/models/commands.py | 82 ++ esphome/components/mipi_spi/models/cyd.py | 10 + esphome/components/mipi_spi/models/ili.py | 749 ++++++++++++++++++ esphome/components/mipi_spi/models/jc.py | 260 ++++++ esphome/components/mipi_spi/models/lanbon.py | 15 + esphome/components/mipi_spi/models/lilygo.py | 60 ++ .../components/mipi_spi/models/waveshare.py | 139 ++++ esphome/components/spi/spi.h | 6 + tests/components/mipi_spi/common.yaml | 38 + .../test-esp32-2432s028.esp32-s3-idf.yaml | 41 + .../test-jc3248w535.esp32-s3-idf.yaml | 41 + .../test-jc3636w518.esp32-s3-idf.yaml | 19 + ...est-pico-restouch-lcd-35.esp32-s3-idf.yaml | 9 + .../mipi_spi/test-s3box.esp32-s3-idf.yaml | 41 + .../mipi_spi/test-s3boxlite.esp32-s3-idf.yaml | 41 + ...t-display-s3-amoled-plus.esp32-s3-idf.yaml | 9 + ...test-t-display-s3-amoled.esp32-s3-idf.yaml | 15 + .../test-t-display-s3-pro.esp32-s3-idf.yaml | 9 + .../test-t-display-s3.esp32-s3-idf.yaml | 37 + .../mipi_spi/test-t-display.esp32-s3-idf.yaml | 41 + .../mipi_spi/test-t-embed.esp32-s3-idf.yaml | 9 + .../mipi_spi/test-t4-s3.esp32-s3-idf.yaml | 41 + .../test-wt32-sc01-plus.esp32-s3-idf.yaml | 37 + tests/components/mipi_spi/test.esp32-ard.yaml | 15 + .../mipi_spi/test.esp32-c3-ard.yaml | 10 + .../mipi_spi/test.esp32-c3-idf.yaml | 10 + tests/components/mipi_spi/test.esp32-idf.yaml | 15 + .../components/mipi_spi/test.rp2040-ard.yaml | 10 + 35 files changed, 3088 insertions(+) create mode 100644 esphome/components/mipi_spi/__init__.py create mode 100644 esphome/components/mipi_spi/display.py create mode 100644 esphome/components/mipi_spi/mipi_spi.cpp create mode 100644 esphome/components/mipi_spi/mipi_spi.h create mode 100644 esphome/components/mipi_spi/models/__init__.py create mode 100644 esphome/components/mipi_spi/models/amoled.py create mode 100644 esphome/components/mipi_spi/models/commands.py create mode 100644 esphome/components/mipi_spi/models/cyd.py create mode 100644 esphome/components/mipi_spi/models/ili.py create mode 100644 esphome/components/mipi_spi/models/jc.py create mode 100644 esphome/components/mipi_spi/models/lanbon.py create mode 100644 esphome/components/mipi_spi/models/lilygo.py create mode 100644 esphome/components/mipi_spi/models/waveshare.py create mode 100644 tests/components/mipi_spi/common.yaml create mode 100644 tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test.esp32-ard.yaml create mode 100644 tests/components/mipi_spi/test.esp32-c3-ard.yaml create mode 100644 tests/components/mipi_spi/test.esp32-c3-idf.yaml create mode 100644 tests/components/mipi_spi/test.esp32-idf.yaml create mode 100644 tests/components/mipi_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e6c149012a..ddd0494a3c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -282,6 +282,7 @@ esphome/components/microphone/* @jesserockz @kahrendt esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov +esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt esphome/components/mlx90393/* @functionpointer diff --git a/esphome/components/mipi_spi/__init__.py b/esphome/components/mipi_spi/__init__.py new file mode 100644 index 0000000000..46b0206a1f --- /dev/null +++ b/esphome/components/mipi_spi/__init__.py @@ -0,0 +1,15 @@ +CODEOWNERS = ["@clydebarrow"] + +DOMAIN = "mipi_spi" + +CONF_DRAW_FROM_ORIGIN = "draw_from_origin" +CONF_SPI_16 = "spi_16" +CONF_PIXEL_MODE = "pixel_mode" +CONF_COLOR_DEPTH = "color_depth" +CONF_BUS_MODE = "bus_mode" +CONF_USE_AXIS_FLIPS = "use_axis_flips" +CONF_NATIVE_WIDTH = "native_width" +CONF_NATIVE_HEIGHT = "native_height" + +MODE_RGB = "RGB" +MODE_BGR = "BGR" diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py new file mode 100644 index 0000000000..e9ed97a2a2 --- /dev/null +++ b/esphome/components/mipi_spi/display.py @@ -0,0 +1,474 @@ +import logging + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE +import esphome.config_validation as cv +from esphome.config_validation import ALLOW_EXTRA +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_ORDER, + CONF_CS_PIN, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_HEIGHT, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.core import TimePeriod + +from ..const import CONF_DRAW_ROUNDING +from ..lvgl.defines import CONF_COLOR_DEPTH +from . import ( + CONF_BUS_MODE, + CONF_DRAW_FROM_ORIGIN, + CONF_NATIVE_HEIGHT, + CONF_NATIVE_WIDTH, + CONF_PIXEL_MODE, + CONF_SPI_16, + CONF_USE_AXIS_FLIPS, + DOMAIN, + MODE_BGR, + MODE_RGB, +) +from .models import ( + DELAY_FLAG, + MADCTL_BGR, + MADCTL_MV, + MADCTL_MX, + MADCTL_MY, + MADCTL_XFLIP, + MADCTL_YFLIP, + DriverChip, + amoled, + cyd, + ili, + jc, + lanbon, + lilygo, + waveshare, +) +from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT + +DEPENDENCIES = ["spi"] + +LOGGER = logging.getLogger(DOMAIN) +mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") +MipiSpi = mipi_spi_ns.class_( + "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") +ColorBitness = display.display_ns.enum("ColorBitness") +Model = mipi_spi_ns.enum("Model") + +COLOR_ORDERS = { + MODE_RGB: ColorOrder.COLOR_ORDER_RGB, + MODE_BGR: ColorOrder.COLOR_ORDER_BGR, +} + +COLOR_DEPTHS = { + 8: ColorBitness.COLOR_BITNESS_332, + 16: ColorBitness.COLOR_BITNESS_565, +} +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + + +DriverChip("CUSTOM", initsequence={}) + +MODELS = DriverChip.models +# These statements are noops, but serve to suppress linting of side-effect-only imports +for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): + pass + +PixelMode = mipi_spi_ns.enum("PixelMode") + +PIXEL_MODE_18BIT = "18bit" +PIXEL_MODE_16BIT = "16bit" + +PIXEL_MODES = { + PIXEL_MODE_16BIT: 0x55, + PIXEL_MODE_18BIT: 0x66, +} + + +def validate_dimension(rounding): + def validator(value): + value = cv.positive_int(value) + if value % rounding != 0: + raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") + return value + + return validator + + +def map_sequence(value): + """ + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A delay can be inserted by specifying "- delay N" where N is in ms + """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return DELAY_FLAG, delay.total_milliseconds + if isinstance(value, int): + return (value,) + value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) + return tuple(value) + + +def power_of_two(value): + value = cv.int_range(1, 128)(value) + if value & (value - 1) != 0: + raise cv.Invalid("value must be a power of two") + return value + + +def dimension_schema(rounding): + return cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): validate_dimension(rounding), + cv.Required(CONF_HEIGHT): validate_dimension(rounding), + cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( + rounding + ), + cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), + } + ), + ) + + +def model_schema(bus_mode, model: DriverChip, swapsies: bool): + transform = cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ) + if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED: + transform = transform.extend( + { + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by this model" + ) + } + ) + else: + transform = transform.extend( + { + cv.Required(CONF_SWAP_XY): cv.boolean, + } + ) + # CUSTOM model will need to provide a custom init sequence + iseqconf = ( + cv.Required(CONF_INIT_SEQUENCE) + if model.initsequence is None + else cv.Optional(CONF_INIT_SEQUENCE) + ) + # Dimensions are optional if the model has a default width and the transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required + ) + pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) + color_depth = ( + ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") + ) + schema = ( + display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE3" if bus_mode == TYPE_OCTAL else "MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), + mode=bus_mode, + ) + ) + .extend( + { + model.option(pin, cv.UNDEFINED): pins.gpio_output_pin_schema + for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_DC_PIN) + } + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MipiSpi), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( + COLOR_ORDERS, upper=True + ), + model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( + cv.one_of(*pixel_modes, lower=True), + cv.int_range(0, 255, min_included=True, max_included=True), + ), + cv.Optional(CONF_TRANSFORM): transform, + cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( + bus_mode, lower=True + ), + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + iseqconf: cv.ensure_list(map_sequence), + } + ) + .extend( + { + model.option(x): cv.boolean + for x in [ + CONF_DRAW_FROM_ORIGIN, + CONF_SPI_16, + CONF_INVERT_COLORS, + CONF_USE_AXIS_FLIPS, + ] + } + ) + ) + if brightness := model.get_default(CONF_BRIGHTNESS): + schema = schema.extend( + { + cv.Optional(CONF_BRIGHTNESS, default=brightness): cv.int_range( + 0, 0xFF, min_included=True, max_included=True + ), + } + ) + if bus_mode != TYPE_SINGLE: + return cv.All(schema, cv.only_with_esp_idf) + return schema + + +def rotation_as_transform(model, config): + """ + Check if a rotation can be implemented in hardware using the MADCTL register. + A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + """ + rotation = config.get(CONF_ROTATION, 0) + return rotation and ( + model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 + ) + + +def config_schema(config): + # First get the model and bus mode + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=ALLOW_EXTRA, + )(config) + model = MODELS[config[CONF_MODEL]] + bus_modes = model.modes + config = cv.Schema( + { + model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=ALLOW_EXTRA, + )(config) + bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) + swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True + config = model_schema(bus_mode, model, swapsies)(config) + # Check for invalid combinations of MADCTL config + if init_sequence := config.get(CONF_INIT_SEQUENCE): + if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: + raise cv.Invalid( + f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" + ) + + if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: + raise cv.Invalid("DC pin is not supported in quad mode") + if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE: + raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus") + if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: + raise cv.Invalid(f"DC pin is required in {bus_mode} mode") + return config + + +CONFIG_SCHEMA = config_schema + + +def get_transform(model, config): + can_transform = rotation_as_transform(model, config) + transform = config.get( + CONF_TRANSFORM, + { + CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False), + CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False), + CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False), + }, + ) + + # Can we use the MADCTL register to set the rotation? + if can_transform and CONF_TRANSFORM not in config: + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + else: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_TRANSFORM] = True + return transform + + +def get_sequence(model, config): + """ + Create the init sequence for the display. + Use the default sequence from the model, if any, and append any custom sequence provided in the config. + Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence + Pixel format, color order, and orientation will be set. + """ + sequence = list(model.initsequence) + custom_sequence = config.get(CONF_INIT_SEQUENCE, []) + sequence.extend(custom_sequence) + # Ensure each command is a tuple + sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] + commands = [x[0] for x in sequence] + # Set pixel format if not already in the custom sequence + if PIXFMT not in commands: + pixel_mode = config[CONF_PIXEL_MODE] + if not isinstance(pixel_mode, int): + pixel_mode = PIXEL_MODES[pixel_mode] + sequence.append((PIXFMT, pixel_mode)) + # Does the chip use the flipping bits for mirroring rather than the reverse order bits? + use_flip = config[CONF_USE_AXIS_FLIPS] + if MADCTL not in commands: + madctl = 0 + transform = get_transform(model, config) + if transform.get(CONF_TRANSFORM): + LOGGER.info("Using hardware transform to implement rotation") + if transform.get(CONF_MIRROR_X): + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + if INVON not in commands and INVOFF not in commands: + if config[CONF_INVERT_COLORS]: + sequence.append((INVON,)) + else: + sequence.append((INVOFF,)) + if BRIGHTNESS not in commands: + if brightness := config.get( + CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS) + ): + sequence.append((BRIGHTNESS, brightness)) + if SLPOUT not in commands: + sequence.append((SLPOUT,)) + sequence.append((DISPON,)) + + # Flatten the sequence into a list of bytes, with the length of each command + # or the delay flag inserted where needed + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ) + + +async def to_code(config): + model = MODELS[config[CONF_MODEL]] + transform = get_transform(model, config) + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + offset_width = dimensions[CONF_OFFSET_WIDTH] + offset_height = dimensions[CONF_OFFSET_HEIGHT] + else: + (width, height) = dimensions + offset_width = 0 + offset_height = 0 + else: + # Default dimensions, use model defaults and transform if needed + width = model.get_default(CONF_WIDTH) + height = model.get_default(CONF_HEIGHT) + offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) + offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) + + # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where + # the offset is asymmetric + if transform[CONF_MIRROR_X]: + native_width = model.get_default( + CONF_NATIVE_WIDTH, width + offset_width * 2 + ) + offset_width = native_width - width - offset_width + if transform[CONF_MIRROR_Y]: + native_height = model.get_default( + CONF_NATIVE_HEIGHT, height + offset_height * 2 + ) + offset_height = native_height - height - offset_height + # Swap default dimensions if swap_xy is set + if transform[CONF_SWAP_XY] is True: + width, height = height, width + offset_height, offset_width = offset_width, offset_height + + color_depth = config[CONF_COLOR_DEPTH] + if color_depth.endswith("bit"): + color_depth = color_depth[:-3] + color_depth = COLOR_DEPTHS[int(color_depth)] + + var = cg.new_Pvariable( + config[CONF_ID], width, height, offset_width, offset_height, color_depth + ) + cg.add(var.set_init_sequence(get_sequence(model, config))) + if rotation_as_transform(model, config): + if CONF_TRANSFORM in config: + LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") + else: + config[CONF_ROTATION] = 0 + cg.add(var.set_model(config[CONF_MODEL])) + cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) + cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) + cg.add(var.set_spi_16(config[CONF_SPI_16])) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if dc_pin := config.get(CONF_DC_PIN): + dc_pin = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc_pin)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + await display.register_display(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp new file mode 100644 index 0000000000..2d393ac349 --- /dev/null +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -0,0 +1,481 @@ +#include "mipi_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mipi_spi { + +void MipiSpi::setup() { + ESP_LOGCONFIG(TAG, "Setting up MIPI SPI"); + this->spi_setup(); + if (this->dc_pin_ != nullptr) { + this->dc_pin_->setup(); + this->dc_pin_->digital_write(false); + } + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } + this->bus_width_ = this->parent_->get_bus_width(); + + // need to know when the display is ready for SLPOUT command - will be 120ms after reset + auto when = millis() + 120; + delay(10); + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + auto arg_byte = vec[index]; + switch (cmd) { + case SLEEP_OUT: { + // are we ready, boots? + int duration = when - millis(); + if (duration > 0) { + ESP_LOGD(TAG, "Sleep %dms", duration); + delay(duration); + } + } break; + + case INVERT_ON: + this->invert_colors_ = true; + break; + case MADCTL_CMD: + this->madctl_ = arg_byte; + break; + case PIXFMT: + this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18; + break; + case BRIGHTNESS: + this->brightness_ = arg_byte; + break; + + default: + break; + } + const auto *ptr = vec.data() + index; + ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); + this->write_command_(cmd, ptr, num_args); + index += num_args; + if (cmd == SLEEP_OUT) + delay(10); + } + } + this->setup_complete_ = true; + if (this->draw_from_origin_) + check_buffer_(); + ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); +} + +void MipiSpi::update() { + if (!this->setup_complete_ || this->is_failed()) { + return; + } + this->do_update_(); + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + // Some chips require that the drawing window be aligned on certain boundaries + auto dr = this->draw_rounding_; + this->x_low_ = this->x_low_ / dr * dr; + this->y_low_ = this->y_low_ / dr * dr; + this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; + this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; + if (this->draw_from_origin_) { + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + } + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_, + this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void MipiSpi::fill(Color color) { + if (!this->check_buffer_()) + return; + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->get_width_internal() - 1; + this->y_high_ = this->get_height_internal() - 1; + switch (this->color_depth_) { + case display::COLOR_BITNESS_332: { + auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); + break; + } + default: { + auto new_color = display::ColorUtil::color_to_565(color); + if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) { + // Upper and lower is equal can use quicker memset operation. Takes ~20ms. + memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); + } else { + auto *ptr_16 = reinterpret_cast(this->buffer_); + auto len = this->buffer_bytes_ / 2; + while (len--) { + *ptr_16++ = new_color; + } + } + } + } +} + +void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + switch (this->color_depth_) { + case display::COLOR_BITNESS_332: { + uint8_t new_color = display::ColorUtil::color_to_332(color); + if (this->buffer_[pos] == new_color) + return; + this->buffer_[pos] = new_color; + break; + } + + case display::COLOR_BITNESS_565: { + 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); + uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + if (ptr_16[pos] == new_color) + return; + ptr_16[pos] = new_color; + break; + } + default: + return; + } + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} + +void MipiSpi::reset_params_() { + if (!this->is_ready()) + return; + this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); + if (this->brightness_.has_value()) + this->write_command_(BRIGHTNESS, this->brightness_.value()); +} + +void MipiSpi::write_init_sequence_() { + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + const auto *ptr = vec.data() + index; + this->write_command_(cmd, ptr, num_args); + index += num_args; + } + } + this->setup_complete_ = true; + ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); +} + +void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); + uint8_t buf[4]; + x1 += this->offset_width_; + x2 += this->offset_width_; + y1 += this->offset_height_; + y2 += this->offset_height_; + put16_be(buf, y1); + put16_be(buf + 2, y2); + this->write_command_(RASET, buf, sizeof buf); + put16_be(buf, x1); + put16_be(buf + 2, x2); + this->write_command_(CASET, buf, sizeof buf); +} + +void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (!this->setup_complete_ || this->is_failed()) + return; + if (w <= 0 || h <= 0) + return; + if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; + } + if (this->draw_from_origin_) { + auto stride = x_offset + w + x_pad; + for (int y = 0; y != h; y++) { + memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2, + ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2); + } + ptr = this->buffer_; + w = this->width_; + h += y_start; + x_start = 0; + y_start = 0; + x_offset = 0; + y_offset = 0; + } + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); +} + +void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) { + stride -= w; + uint8_t transfer_buffer[6 * 256]; + size_t idx = 0; // index into transfer_buffer + while (h-- != 0) { + for (auto x = w; x-- != 0;) { + auto color_val = *ptr++; + // deal with byte swapping + transfer_buffer[idx++] = (color_val & 0xF8); // Blue + transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green + transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + } + } + ptr += stride; + } + if (idx != 0) + this->write_array(transfer_buffer, idx); +} + +void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { + stride -= w; + uint8_t transfer_buffer[6 * 256]; + size_t idx = 0; // index into transfer_buffer + while (h-- != 0) { + for (auto x = w; x-- != 0;) { + auto color_val = *ptr++; + transfer_buffer[idx++] = color_val & 0xE0; // Red + transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green + transfer_buffer[idx++] = color_val << 6; // Blue + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + } + } + ptr += stride; + } + if (idx != 0) + this->write_array(transfer_buffer, idx); +} + +void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { + stride -= w; + uint8_t transfer_buffer[6 * 256]; + size_t idx = 0; // index into transfer_buffer + while (h-- != 0) { + for (auto x = w; x-- != 0;) { + auto color_val = *ptr++; + transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); + transfer_buffer[idx++] = (color_val & 0x3) << 3; + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + } + } + ptr += stride; + } + if (idx != 0) + this->write_array(transfer_buffer, idx); +} + +void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); + auto stride = x_offset + w + x_pad; + const auto *offset_ptr = ptr; + if (this->color_depth_ == display::COLOR_BITNESS_332) { + offset_ptr += y_offset * stride + x_offset; + } else { + stride *= 2; + offset_ptr += y_offset * stride + x_offset * 2; + } + + switch (this->bus_width_) { + case 4: + this->enable(); + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't + // bother + this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4); + } else { + this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4); + for (int y = 0; y != h; y++) { + this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4); + offset_ptr += stride; + } + } + break; + + case 8: + this->write_command_(WDATA); + this->enable(); + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8); + } else { + for (int y = 0; y != h; y++) { + this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8); + offset_ptr += stride; + } + } + break; + + default: + this->write_command_(WDATA); + this->enable(); + + if (this->color_depth_ == display::COLOR_BITNESS_565) { + // Source buffer is 16-bit RGB565 + if (this->pixel_mode_ == PIXEL_MODE_18) { + // Convert RGB565 to RGB666 + this->write_18_from_16_bit_(reinterpret_cast(offset_ptr), w, h, stride / 2); + } else { + // Direct RGB565 output + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + this->write_array(ptr, w * h * 2); + } else { + for (int y = 0; y != h; y++) { + this->write_array(offset_ptr, w * 2); + offset_ptr += stride; + } + } + } + } else { + // Source buffer is 8-bit RGB332 + if (this->pixel_mode_ == PIXEL_MODE_18) { + // Convert RGB332 to RGB666 + this->write_18_from_8_bit_(offset_ptr, w, h, stride); + } else { + this->write_16_from_8_bit_(offset_ptr, w, h, stride); + } + break; + } + } + this->disable(); +} + +void MipiSpi::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 (this->bus_width_ == 4) { + this->enable(); + this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); + this->disable(); + } else if (this->bus_width_ == 8) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); + this->disable(); + this->dc_pin_->digital_write(true); + if (len != 0) { + this->enable(); + this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); + this->disable(); + } + } else { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(cmd); + this->disable(); + this->dc_pin_->digital_write(true); + if (len != 0) { + if (this->spi_16_) { + for (size_t i = 0; i != len; i++) { + this->enable(); + this->write_byte(0); + this->write_byte(bytes[i]); + this->disable(); + } + } else { + this->enable(); + this->write_array(bytes, len); + this->disable(); + } + } + } +} + +void MipiSpi::dump_config() { + ESP_LOGCONFIG(TAG, "MIPI_SPI Display"); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_); + ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + ESP_LOGCONFIG(TAG, " Height: %u", this->height_); + if (this->offset_width_ != 0) + ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_); + if (this->offset_height_ != 0) + ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_); + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->madctl_ & MADCTL_MV)); + ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP))); + ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP))); + ESP_LOGCONFIG(TAG, " Color depth: %d bits", this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8); + ESP_LOGCONFIG(TAG, " Invert colors: %s", YESNO(this->invert_colors_)); + ESP_LOGCONFIG(TAG, " Color order: %s", this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); + ESP_LOGCONFIG(TAG, " Pixel mode: %s", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit"); + if (this->brightness_.has_value()) + ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value()); + if (this->spi_16_) + ESP_LOGCONFIG(TAG, " SPI 16bit: YES"); + ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_); + if (this->draw_from_origin_) + ESP_LOGCONFIG(TAG, " Draw from origin: YES"); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + ESP_LOGCONFIG(TAG, " SPI Mode: %d", this->mode_); + ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", static_cast(this->data_rate_ / 1000000)); + ESP_LOGCONFIG(TAG, " SPI Bus width: %d", this->bus_width_); +} + +} // namespace mipi_spi +} // namespace esphome diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h new file mode 100644 index 0000000000..052ebe3a6b --- /dev/null +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -0,0 +1,171 @@ +#pragma once + +#include + +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display_color_utils.h" + +namespace esphome { +namespace mipi_spi { + +constexpr static const char *const TAG = "display.mipi_spi"; +static const uint8_t SW_RESET_CMD = 0x01; +static const uint8_t SLEEP_OUT = 0x11; +static const uint8_t NORON = 0x13; +static const uint8_t INVERT_OFF = 0x20; +static const uint8_t INVERT_ON = 0x21; +static const uint8_t ALL_ON = 0x23; +static const uint8_t WRAM = 0x24; +static const uint8_t MIPI = 0x26; +static const uint8_t DISPLAY_ON = 0x29; +static const uint8_t RASET = 0x2B; +static const uint8_t CASET = 0x2A; +static const uint8_t WDATA = 0x2C; +static const uint8_t TEON = 0x35; +static const uint8_t MADCTL_CMD = 0x36; +static const uint8_t PIXFMT = 0x3A; +static const uint8_t BRIGHTNESS = 0x51; +static const uint8_t SWIRE1 = 0x5A; +static const uint8_t SWIRE2 = 0x5B; +static const uint8_t PAGESEL = 0xFE; + +static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order +static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +static const uint8_t DELAY_FLAG = 0xFF; +// store a 16 bit value in a buffer, big endian. +static inline void put16_be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} + +enum PixelMode { + PIXEL_MODE_16, + PIXEL_MODE_18, +}; + +class MipiSpi : public display::DisplayBuffer, + public spi::SPIDevice { + public: + MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) + : width_(width), + height_(height), + offset_width_(offset_width), + offset_height_(offset_height), + color_depth_(color_depth) {} + void set_model(const char *model) { this->model_ = model; } + void update() override; + void setup() override; + display::ColorOrder get_color_mode() { + return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + } + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_invert_colors(bool invert_colors) { + this->invert_colors_ = invert_colors; + this->reset_params_(); + } + void set_brightness(uint8_t brightness) { + this->brightness_ = brightness; + this->reset_params_(); + } + + void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void dump_config() override; + + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + bool can_proceed() override { return this->setup_complete_; } + void set_init_sequence(const std::vector &sequence) { this->init_sequence_ = sequence; } + void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } + void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; } + + protected: + bool check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1; + this->init_internal_(this->width_ * this->height_ * bytes_per_pixel); + if (this->buffer_ == nullptr) { + this->mark_failed(); + return false; + } + this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel; + return true; + } + void fill(Color color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride); + void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); + void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + /** + * the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the + * sample code.) + * + * Immediately after enabling /CS send 4 bytes in single-dataline SPI mode: + * 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be + * sent in 1-dataline SPI. The second indicates quad mode. + * 1: 0x00 + * 2: The command (register address) byte. + * 3: 0x00 + * + * This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte. + * At the conclusion of the write, de-assert /CS. + * + * @param cmd + * @param bytes + * @param len + */ + void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len); + + void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } + void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } + void reset_params_(); + void write_init_sequence_(); + void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2); + + GPIOPin *reset_pin_{nullptr}; + std::vector enable_pins_{}; + GPIOPin *dc_pin_{nullptr}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + bool setup_complete_{}; + + bool invert_colors_{}; + size_t width_; + size_t height_; + int16_t offset_width_; + int16_t offset_height_; + size_t buffer_bytes_{0}; + display::ColorBitness color_depth_; + PixelMode pixel_mode_{PIXEL_MODE_16}; + uint8_t bus_width_{}; + bool spi_16_{}; + uint8_t madctl_{}; + bool draw_from_origin_{false}; + unsigned draw_rounding_{2}; + optional brightness_{}; + const char *model_{"Unknown"}; + std::vector init_sequence_{}; +}; +} // namespace mipi_spi +} // namespace esphome diff --git a/esphome/components/mipi_spi/models/__init__.py b/esphome/components/mipi_spi/models/__init__.py new file mode 100644 index 0000000000..e9726032d4 --- /dev/null +++ b/esphome/components/mipi_spi/models/__init__.py @@ -0,0 +1,65 @@ +from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH + +from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH + +MADCTL_MY = 0x80 # Bit 7 Bottom to top +MADCTL_MX = 0x40 # Bit 6 Right to left +MADCTL_MV = 0x20 # Bit 5 Reverse Mode +MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top +MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order +MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order +MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left + +# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect +# partial updates. +MADCTL_XFLIP = 0x02 # Mirror the display horizontally +MADCTL_YFLIP = 0x01 # Mirror the display vertically + +DELAY_FLAG = 0xFFF # Special flag to indicate a delay + + +def delay(ms): + return DELAY_FLAG, ms + + +class DriverChip: + models = {} + + def __init__( + self, + name: str, + modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL), + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.modes = modes + self.initsequence = initsequence + self.defaults = defaults + DriverChip.models[name] = self + + def extend(self, name, **kwargs): + defaults = self.defaults.copy() + if ( + CONF_WIDTH in defaults + and CONF_OFFSET_WIDTH in kwargs + and CONF_NATIVE_WIDTH not in defaults + ): + defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] + if ( + CONF_HEIGHT in defaults + and CONF_OFFSET_HEIGHT in kwargs + and CONF_NATIVE_HEIGHT not in defaults + ): + defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] + defaults.update(kwargs) + return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults) + + def get_default(self, key, fallback=False): + return self.defaults.get(key, fallback) + + def option(self, name, fallback=False): + return cv.Optional(name, default=self.get_default(name, fallback)) diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py new file mode 100644 index 0000000000..14277b243f --- /dev/null +++ b/esphome/components/mipi_spi/models/amoled.py @@ -0,0 +1,72 @@ +from esphome.components.spi import TYPE_QUAD + +from .. import MODE_RGB +from . import DriverChip, delay +from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM + +DriverChip( + "T-DISPLAY-S3-AMOLED", + width=240, + height=536, + cs_pin=6, + reset_pin=17, + enable_pin=38, + bus_mode=TYPE_QUAD, + brightness=0xD0, + color_order=MODE_RGB, + initsequence=(SLPOUT,), # Requires early SLPOUT +) + +DriverChip( + name="T-DISPLAY-S3-AMOLED-PLUS", + width=240, + height=536, + cs_pin=6, + reset_pin=17, + dc_pin=7, + enable_pin=38, + data_rate="40MHz", + brightness=0xD0, + color_order=MODE_RGB, + initsequence=( + (PAGESEL, 4), + (0x6A, 0x00), + (PAGESEL, 0x05), + (PAGESEL, 0x07), + (0x07, 0x4F), + (PAGESEL, 0x01), + (0x2A, 0x02), + (0x2B, 0x73), + (PAGESEL, 0x0A), + (0x29, 0x10), + (PAGESEL, 0x00), + (0x53, 0x20), + (TEON, 0x00), + (PIXFMT, 0x75), + (0xC4, 0x80), + ), +) + +RM690B0 = DriverChip( + "RM690B0", + brightness=0xD0, + color_order=MODE_RGB, + width=480, + height=600, + initsequence=( + (PAGESEL, 0x20), + (MIPI, 0x0A), + (WRAM, 0x80), + (SWIRE1, 0x51), + (SWIRE2, 0x2E), + (PAGESEL, 0x00), + (0xC2, 0x00), + delay(10), + (TEON, 0x00), + (NORON,), + ), +) + +T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) + +models = {} diff --git a/esphome/components/mipi_spi/models/commands.py b/esphome/components/mipi_spi/models/commands.py new file mode 100644 index 0000000000..032a6e6b2b --- /dev/null +++ b/esphome/components/mipi_spi/models/commands.py @@ -0,0 +1,82 @@ +# MIPI DBI commands + +NOP = 0x00 +SWRESET = 0x01 +RDDID = 0x04 +RDDST = 0x09 +RDMODE = 0x0A +RDMADCTL = 0x0B +RDPIXFMT = 0x0C +RDIMGFMT = 0x0D +RDSELFDIAG = 0x0F +SLEEP_IN = 0x10 +SLPIN = 0x10 +SLEEP_OUT = 0x11 +SLPOUT = 0x11 +PTLON = 0x12 +NORON = 0x13 +INVERT_OFF = 0x20 +INVOFF = 0x20 +INVERT_ON = 0x21 +INVON = 0x21 +ALL_ON = 0x23 +WRAM = 0x24 +GAMMASET = 0x26 +MIPI = 0x26 +DISPOFF = 0x28 +DISPON = 0x29 +CASET = 0x2A +PASET = 0x2B +RASET = 0x2B +RAMWR = 0x2C +WDATA = 0x2C +RAMRD = 0x2E +PTLAR = 0x30 +VSCRDEF = 0x33 +TEON = 0x35 +MADCTL = 0x36 +MADCTL_CMD = 0x36 +VSCRSADD = 0x37 +IDMOFF = 0x38 +IDMON = 0x39 +COLMOD = 0x3A +PIXFMT = 0x3A +GETSCANLINE = 0x45 +BRIGHTNESS = 0x51 +WRDISBV = 0x51 +RDDISBV = 0x52 +WRCTRLD = 0x53 +SWIRE1 = 0x5A +SWIRE2 = 0x5B +IFMODE = 0xB0 +FRMCTR1 = 0xB1 +FRMCTR2 = 0xB2 +FRMCTR3 = 0xB3 +INVCTR = 0xB4 +DFUNCTR = 0xB6 +ETMOD = 0xB7 +PWCTR1 = 0xC0 +PWCTR2 = 0xC1 +PWCTR3 = 0xC2 +PWCTR4 = 0xC3 +PWCTR5 = 0xC4 +VMCTR1 = 0xC5 +IFCTR = 0xC6 +VMCTR2 = 0xC7 +GMCTR = 0xC8 +SETEXTC = 0xC8 +PWSET = 0xD0 +VMCTR = 0xD1 +PWSETN = 0xD2 +RDID4 = 0xD3 +RDINDEX = 0xD9 +RDID1 = 0xDA +RDID2 = 0xDB +RDID3 = 0xDC +RDIDX = 0xDD +GMCTRP1 = 0xE0 +GMCTRN1 = 0xE1 +CSCON = 0xF0 +PWCTR6 = 0xF6 +ADJCTL3 = 0xF7 +PAGESEL = 0xFE diff --git a/esphome/components/mipi_spi/models/cyd.py b/esphome/components/mipi_spi/models/cyd.py new file mode 100644 index 0000000000..a25ecf33a8 --- /dev/null +++ b/esphome/components/mipi_spi/models/cyd.py @@ -0,0 +1,10 @@ +from .ili import ILI9341 + +ILI9341.extend( + "ESP32-2432S028", + data_rate="40MHz", + cs_pin=15, + dc_pin=2, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py new file mode 100644 index 0000000000..cc12b38f5d --- /dev/null +++ b/esphome/components/mipi_spi/models/ili.py @@ -0,0 +1,749 @@ +from esphome.components.spi import TYPE_OCTAL + +from .. import MODE_RGB +from . import DriverChip, delay +from .commands import ( + ADJCTL3, + CSCON, + DFUNCTR, + ETMOD, + FRMCTR1, + FRMCTR2, + FRMCTR3, + GAMMASET, + GMCTR, + GMCTRN1, + GMCTRP1, + IDMOFF, + IFCTR, + IFMODE, + INVCTR, + NORON, + PWCTR1, + PWCTR2, + PWCTR3, + PWCTR4, + PWCTR5, + PWSET, + PWSETN, + SETEXTC, + SWRESET, + VMCTR, + VMCTR1, + VMCTR2, + VSCRSADD, +) + +DriverChip( + "M5CORE", + width=320, + height=240, + cs_pin=14, + dc_pin=27, + reset_pin=33, + initsequence=( + (SETEXTC, 0xFF, 0x93, 0x42), + (PWCTR1, 0x12, 0x12), + (PWCTR2, 0x03), + (VMCTR1, 0xF2), + (IFMODE, 0xE0), + (0xF6, 0x01, 0x00, 0x00), + ( + GMCTRP1, + 0x00, + 0x0C, + 0x11, + 0x04, + 0x11, + 0x08, + 0x37, + 0x89, + 0x4C, + 0x06, + 0x0C, + 0x0A, + 0x2E, + 0x34, + 0x0F, + ), + ( + GMCTRN1, + 0x00, + 0x0B, + 0x11, + 0x05, + 0x13, + 0x09, + 0x33, + 0x67, + 0x48, + 0x07, + 0x0E, + 0x0B, + 0x2E, + 0x33, + 0x0F, + ), + (DFUNCTR, 0x08, 0x82, 0x1D, 0x04), + (IDMOFF,), + ), +) +ILI9341 = DriverChip( + "ILI9341", + mirror_x=True, + width=240, + height=320, + initsequence=( + (0xEF, 0x03, 0x80, 0x02), + (0xCF, 0x00, 0xC1, 0x30), + (0xED, 0x64, 0x03, 0x12, 0x81), + (0xE8, 0x85, 0x00, 0x78), + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), + (0xF7, 0x20), + (0xEA, 0x00, 0x00), + (PWCTR1, 0x23), + (PWCTR2, 0x10), + (VMCTR1, 0x3E, 0x28), + (VMCTR2, 0x86), + (VSCRSADD, 0x00), + (FRMCTR1, 0x00, 0x18), + (DFUNCTR, 0x08, 0x82, 0x27), + (0xF2, 0x00), + (GAMMASET, 0x01), + ( + GMCTRP1, + 0x0F, + 0x31, + 0x2B, + 0x0C, + 0x0E, + 0x08, + 0x4E, + 0xF1, + 0x37, + 0x07, + 0x10, + 0x03, + 0x0E, + 0x09, + 0x00, + ), + ( + GMCTRN1, + 0x00, + 0x0E, + 0x14, + 0x03, + 0x11, + 0x07, + 0x31, + 0xC1, + 0x48, + 0x08, + 0x0F, + 0x0C, + 0x31, + 0x36, + 0x0F, + ), + ), +) +DriverChip( + "ILI9481", + mirror_x=True, + width=320, + height=480, + use_axis_flips=True, + initsequence=( + (PWSET, 0x07, 0x42, 0x18), + (VMCTR, 0x00, 0x07, 0x10), + (PWSETN, 0x01, 0x02), + (PWCTR1, 0x10, 0x3B, 0x00, 0x02, 0x11), + (VMCTR1, 0x03), + (IFCTR, 0x83), + (GMCTR, 0x32, 0x36, 0x45, 0x06, 0x16, 0x37, 0x75, 0x77, 0x54, 0x0C, 0x00), + ), +) +DriverChip( + "ILI9486", + mirror_x=True, + width=320, + height=480, + initsequence=( + (PWCTR3, 0x44), + (VMCTR1, 0x00, 0x00, 0x00, 0x00), + ( + GMCTRP1, + 0x0F, + 0x1F, + 0x1C, + 0x0C, + 0x0F, + 0x08, + 0x48, + 0x98, + 0x37, + 0x0A, + 0x13, + 0x04, + 0x11, + 0x0D, + 0x00, + ), + ( + GMCTRN1, + 0x0F, + 0x32, + 0x2E, + 0x0B, + 0x0D, + 0x05, + 0x47, + 0x75, + 0x37, + 0x06, + 0x10, + 0x03, + 0x24, + 0x20, + 0x00, + ), + ), +) +DriverChip( + "ILI9488", + width=320, + height=480, + pixel_mode="18bit", + initsequence=( + ( + GMCTRP1, + 0x0F, + 0x24, + 0x1C, + 0x0A, + 0x0F, + 0x08, + 0x43, + 0x88, + 0x32, + 0x0F, + 0x10, + 0x06, + 0x0F, + 0x07, + 0x00, + ), + ( + GMCTRN1, + 0x0F, + 0x38, + 0x30, + 0x09, + 0x0F, + 0x0F, + 0x4E, + 0x77, + 0x3C, + 0x07, + 0x10, + 0x05, + 0x23, + 0x1B, + 0x00, + ), + (PWCTR1, 0x17, 0x15), + (PWCTR2, 0x41), + (VMCTR1, 0x00, 0x12, 0x80), + (IFMODE, 0x00), + (FRMCTR1, 0xA0), + (INVCTR, 0x02), + (0xE9, 0x00), + (ADJCTL3, 0xA9, 0x51, 0x2C, 0x82), + ), +) +ILI9488_A = DriverChip( + "ILI9488_A", + width=320, + height=480, + invert_colors=False, + pixel_mode="18bit", + mirror_x=True, + initsequence=( + ( + GMCTRP1, + 0x00, + 0x03, + 0x09, + 0x08, + 0x16, + 0x0A, + 0x3F, + 0x78, + 0x4C, + 0x09, + 0x0A, + 0x08, + 0x16, + 0x1A, + 0x0F, + ), + ( + GMCTRN1, + 0x00, + 0x16, + 0x19, + 0x03, + 0x0F, + 0x05, + 0x32, + 0x45, + 0x46, + 0x04, + 0x0E, + 0x0D, + 0x35, + 0x37, + 0x0F, + ), + (PWCTR1, 0x17, 0x15), + (PWCTR2, 0x41), + (VMCTR1, 0x00, 0x12, 0x80), + (IFMODE, 0x00), + (FRMCTR1, 0xA0), + (INVCTR, 0x02), + (DFUNCTR, 0x02, 0x02), + (0xE9, 0x00), + (ADJCTL3, 0xA9, 0x51, 0x2C, 0x82), + ), +) +ST7796 = DriverChip( + "ST7796", + mirror_x=True, + width=320, + height=480, + initsequence=( + (SWRESET,), + (CSCON, 0xC3), + (CSCON, 0x96), + (VMCTR1, 0x1C), + (IFMODE, 0x80), + (INVCTR, 0x01), + (DFUNCTR, 0x80, 0x02, 0x3B), + (ETMOD, 0xC6), + (CSCON, 0x69), + (CSCON, 0x3C), + ), +) +DriverChip( + "S3BOX", + width=320, + height=240, + mirror_x=True, + mirror_y=True, + invert_colors=False, + data_rate="40MHz", + dc_pin=4, + cs_pin=5, + # reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48}, + initsequence=( + (0xEF, 0x03, 0x80, 0x02), + (0xCF, 0x00, 0xC1, 0x30), + (0xED, 0x64, 0x03, 0x12, 0x81), + (0xE8, 0x85, 0x00, 0x78), + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), + (0xF7, 0x20), + (0xEA, 0x00, 0x00), + (PWCTR1, 0x23), + (PWCTR2, 0x10), + (VMCTR1, 0x3E, 0x28), + (VMCTR2, 0x86), + (VSCRSADD, 0x00), + (FRMCTR1, 0x00, 0x18), + (DFUNCTR, 0x08, 0x82, 0x27), + (0xF2, 0x00), + (GAMMASET, 0x01), + ( + GMCTRP1, + 0x0F, + 0x31, + 0x2B, + 0x0C, + 0x0E, + 0x08, + 0x4E, + 0xF1, + 0x37, + 0x07, + 0x10, + 0x03, + 0x0E, + 0x09, + 0x00, + ), + ( + GMCTRN1, + 0x00, + 0x0E, + 0x14, + 0x03, + 0x11, + 0x07, + 0x31, + 0xC1, + 0x48, + 0x08, + 0x0F, + 0x0C, + 0x31, + 0x36, + 0x0F, + ), + ), +) +DriverChip( + "S3BOXLITE", + mirror_x=True, + color_order=MODE_RGB, + width=320, + height=240, + cs_pin=5, + dc_pin=4, + reset_pin=48, + initsequence=( + (0xEF, 0x03, 0x80, 0x02), + (0xCF, 0x00, 0xC1, 0x30), + (0xED, 0x64, 0x03, 0x12, 0x81), + (0xE8, 0x85, 0x00, 0x78), + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), + (0xF7, 0x20), + (0xEA, 0x00, 0x00), + (PWCTR1, 0x23), + (PWCTR2, 0x10), + (VMCTR1, 0x3E, 0x28), + (VMCTR2, 0x86), + (VSCRSADD, 0x00), + (FRMCTR1, 0x00, 0x18), + (DFUNCTR, 0x08, 0x82, 0x27), + (0xF2, 0x00), + (GAMMASET, 0x01), + ( + GMCTRP1, + 0xF0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x15, + 0x2F, + 0x54, + 0x42, + 0x3C, + 0x17, + 0x14, + 0x18, + 0x1B, + ), + ( + GMCTRN1, + 0xE0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x03, + 0x2B, + 0x43, + 0x42, + 0x3B, + 0x16, + 0x14, + 0x17, + 0x1B, + ), + ), +) +ST7789V = DriverChip( + "ST7789V", + width=240, + height=320, + initsequence=( + (DFUNCTR, 0x0A, 0x82), + (FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33), + (ETMOD, 0x35), + (0xBB, 0x28), + (PWCTR1, 0x0C), + (PWCTR3, 0x01, 0xFF), + (PWCTR4, 0x10), + (PWCTR5, 0x20), + (IFCTR, 0x0F), + (PWSET, 0xA4, 0xA1), + ( + GMCTRP1, + 0xD0, + 0x00, + 0x02, + 0x07, + 0x0A, + 0x28, + 0x32, + 0x44, + 0x42, + 0x06, + 0x0E, + 0x12, + 0x14, + 0x17, + ), + ( + GMCTRN1, + 0xD0, + 0x00, + 0x02, + 0x07, + 0x0A, + 0x28, + 0x31, + 0x54, + 0x47, + 0x0E, + 0x1C, + 0x17, + 0x1B, + 0x1E, + ), + ), +) +DriverChip( + "GC9A01A", + mirror_x=True, + width=240, + height=240, + initsequence=( + (0xEF,), + (0xEB, 0x14), + (0xFE,), + (0xEF,), + (0xEB, 0x14), + (0x84, 0x40), + (0x85, 0xFF), + (0x86, 0xFF), + (0x87, 0xFF), + (0x88, 0x0A), + (0x89, 0x21), + (0x8A, 0x00), + (0x8B, 0x80), + (0x8C, 0x01), + (0x8D, 0x01), + (0x8E, 0xFF), + (0x8F, 0xFF), + (0xB6, 0x00, 0x00), + (0x90, 0x08, 0x08, 0x08, 0x08), + (0xBD, 0x06), + (0xBC, 0x00), + (0xFF, 0x60, 0x01, 0x04), + (0xC3, 0x13), + (0xC4, 0x13), + (0xF9, 0x22), + (0xBE, 0x11), + (0xE1, 0x10, 0x0E), + (0xDF, 0x21, 0x0C, 0x02), + (0xF0, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A), + (0xF1, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F), + (0xF2, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A), + (0xF3, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F), + (0xED, 0x1B, 0x0B), + (0xAE, 0x77), + (0xCD, 0x63), + (0xE8, 0x34), + ( + 0x62, + 0x18, + 0x0D, + 0x71, + 0xED, + 0x70, + 0x70, + 0x18, + 0x0F, + 0x71, + 0xEF, + 0x70, + 0x70, + ), + ( + 0x63, + 0x18, + 0x11, + 0x71, + 0xF1, + 0x70, + 0x70, + 0x18, + 0x13, + 0x71, + 0xF3, + 0x70, + 0x70, + ), + (0x64, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07), + (0x66, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00), + (0x67, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98), + (0x74, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00), + (0x98, 0x3E, 0x07), + (0x35,), + ), +) +DriverChip( + "GC9D01N", + width=160, + height=160, + initsequence=( + (0xFE,), + (0xEF,), + (0x80, 0xFF), + (0x81, 0xFF), + (0x82, 0xFF), + (0x83, 0xFF), + (0x84, 0xFF), + (0x85, 0xFF), + (0x86, 0xFF), + (0x87, 0xFF), + (0x88, 0xFF), + (0x89, 0xFF), + (0x8A, 0xFF), + (0x8B, 0xFF), + (0x8C, 0xFF), + (0x8D, 0xFF), + (0x8E, 0xFF), + (0x8F, 0xFF), + (0x3A, 0x05), + (0xEC, 0x01), + (0x74, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00), + (0x98, 0x3E), + (0x99, 0x3E), + (0xB5, 0x0D, 0x0D), + (0x60, 0x38, 0x0F, 0x79, 0x67), + (0x61, 0x38, 0x11, 0x79, 0x67), + (0x64, 0x38, 0x17, 0x71, 0x5F, 0x79, 0x67), + (0x65, 0x38, 0x13, 0x71, 0x5B, 0x79, 0x67), + (0x6A, 0x00, 0x00), + (0x6C, 0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50), + ( + 0x6E, + 0x03, + 0x03, + 0x01, + 0x01, + 0x00, + 0x00, + 0x0F, + 0x0F, + 0x0D, + 0x0D, + 0x0B, + 0x0B, + 0x09, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x0A, + 0x0C, + 0x0C, + 0x0E, + 0x0E, + 0x10, + 0x10, + 0x00, + 0x00, + 0x02, + 0x02, + 0x04, + 0x04, + ), + (0xBF, 0x01), + (0xF9, 0x40), + (0x9B, 0x3B, 0x93, 0x33, 0x7F, 0x00), + (0x7E, 0x30), + (0x70, 0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08), + (0x71, 0x0D, 0x02, 0x08), + (0x91, 0x0E, 0x09), + (0xC3, 0x19, 0xC4, 0x19, 0xC9, 0x3C), + (0xF0, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E), + (0xF1, 0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F), + (0xF2, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A), + (0xF3, 0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF), + ), +) +DriverChip( + "ST7735", + color_order=MODE_RGB, + width=128, + height=160, + initsequence=( + SWRESET, + delay(10), + (FRMCTR1, 0x01, 0x2C, 0x2D), + (FRMCTR2, 0x01, 0x2C, 0x2D), + (FRMCTR3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D), + (INVCTR, 0x07), + (PWCTR1, 0xA2, 0x02, 0x84), + (PWCTR2, 0xC5), + (PWCTR3, 0x0A, 0x00), + (PWCTR4, 0x8A, 0x2A), + (PWCTR5, 0x8A, 0xEE), + (VMCTR1, 0x0E), + ( + GMCTRP1, + 0x02, + 0x1C, + 0x07, + 0x12, + 0x37, + 0x32, + 0x29, + 0x2D, + 0x29, + 0x25, + 0x2B, + 0x39, + 0x00, + 0x01, + 0x03, + 0x10, + ), + ( + GMCTRN1, + 0x03, + 0x1D, + 0x07, + 0x06, + 0x2E, + 0x2C, + 0x29, + 0x2D, + 0x2E, + 0x2E, + 0x37, + 0x3F, + 0x00, + 0x00, + 0x02, + 0x10, + ), + NORON, + ), +) +ST7796.extend( + "WT32-SC01-PLUS", + bus_mode=TYPE_OCTAL, + mirror_x=True, + reset_pin=4, + dc_pin=0, + invert_colors=True, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py new file mode 100644 index 0000000000..449c5b87ae --- /dev/null +++ b/esphome/components/mipi_spi/models/jc.py @@ -0,0 +1,260 @@ +from esphome.components.spi import TYPE_QUAD +import esphome.config_validation as cv +from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER + +from .. import MODE_RGB +from . import DriverChip + +AXS15231 = DriverChip( + "AXS15231", + draw_rounding=8, + swap_xy=cv.UNDEFINED, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + initsequence=( + (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5), + (0xC1, 0x33), + (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + ), +) + +AXS15231.extend( + "JC3248W535", + width=320, + height=480, + cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True}, + data_rate="40MHz", +) + +DriverChip( + "JC3636W518", + height=360, + width=360, + offset_height=1, + draw_rounding=1, + cs_pin=10, + reset_pin=47, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="40MHz", + initsequence=( + (0xF0, 0x08), + (0xF2, 0x08), + (0x9B, 0x51), + (0x86, 0x53), + (0xF2, 0x80), + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + (0xB0, 0x54), + (0xB1, 0x3F), + (0xB2, 0x2A), + (0xB4, 0x46), + (0xB5, 0x34), + (0xB6, 0xD5), + (0xB7, 0x30), + (0xBA, 0x00), + (0xBB, 0x08), + (0xBC, 0x08), + (0xBD, 0x00), + (0xC0, 0x80), + (0xC1, 0x10), + (0xC2, 0x37), + (0xC3, 0x80), + (0xC4, 0x10), + (0xC5, 0x37), + (0xC6, 0xA9), + (0xC7, 0x41), + (0xC8, 0x51), + (0xC9, 0xA9), + (0xCA, 0x41), + (0xCB, 0x51), + (0xD0, 0x91), + (0xD1, 0x68), + (0xD2, 0x69), + (0xF5, 0x00, 0xA5), + (0xDD, 0x3F), + (0xDE, 0x3F), + (0xF1, 0x10), + (0xF0, 0x00), + (0xF0, 0x02), + ( + 0xE0, + 0x70, + 0x09, + 0x12, + 0x0C, + 0x0B, + 0x27, + 0x38, + 0x54, + 0x4E, + 0x19, + 0x15, + 0x15, + 0x2C, + 0x2F, + ), + ( + 0xE1, + 0x70, + 0x08, + 0x11, + 0x0C, + 0x0B, + 0x27, + 0x38, + 0x43, + 0x4C, + 0x18, + 0x14, + 0x14, + 0x2B, + 0x2D, + ), + (0xF0, 0x10), + (0xF3, 0x10), + (0xE0, 0x08), + (0xE1, 0x00), + (0xE2, 0x00), + (0xE3, 0x00), + (0xE4, 0xE0), + (0xE5, 0x06), + (0xE6, 0x21), + (0xE7, 0x00), + (0xE8, 0x05), + (0xE9, 0x82), + (0xEA, 0xDF), + (0xEB, 0x89), + (0xEC, 0x20), + (0xED, 0x14), + (0xEE, 0xFF), + (0xEF, 0x00), + (0xF8, 0xFF), + (0xF9, 0x00), + (0xFA, 0x00), + (0xFB, 0x30), + (0xFC, 0x00), + (0xFD, 0x00), + (0xFE, 0x00), + (0xFF, 0x00), + (0x60, 0x42), + (0x61, 0xE0), + (0x62, 0x40), + (0x63, 0x40), + (0x64, 0x02), + (0x65, 0x00), + (0x66, 0x40), + (0x67, 0x03), + (0x68, 0x00), + (0x69, 0x00), + (0x6A, 0x00), + (0x6B, 0x00), + (0x70, 0x42), + (0x71, 0xE0), + (0x72, 0x40), + (0x73, 0x40), + (0x74, 0x02), + (0x75, 0x00), + (0x76, 0x40), + (0x77, 0x03), + (0x78, 0x00), + (0x79, 0x00), + (0x7A, 0x00), + (0x7B, 0x00), + (0x80, 0x48), + (0x81, 0x00), + (0x82, 0x05), + (0x83, 0x02), + (0x84, 0xDD), + (0x85, 0x00), + (0x86, 0x00), + (0x87, 0x00), + (0x88, 0x48), + (0x89, 0x00), + (0x8A, 0x07), + (0x8B, 0x02), + (0x8C, 0xDF), + (0x8D, 0x00), + (0x8E, 0x00), + (0x8F, 0x00), + (0x90, 0x48), + (0x91, 0x00), + (0x92, 0x09), + (0x93, 0x02), + (0x94, 0xE1), + (0x95, 0x00), + (0x96, 0x00), + (0x97, 0x00), + (0x98, 0x48), + (0x99, 0x00), + (0x9A, 0x0B), + (0x9B, 0x02), + (0x9C, 0xE3), + (0x9D, 0x00), + (0x9E, 0x00), + (0x9F, 0x00), + (0xA0, 0x48), + (0xA1, 0x00), + (0xA2, 0x04), + (0xA3, 0x02), + (0xA4, 0xDC), + (0xA5, 0x00), + (0xA6, 0x00), + (0xA7, 0x00), + (0xA8, 0x48), + (0xA9, 0x00), + (0xAA, 0x06), + (0xAB, 0x02), + (0xAC, 0xDE), + (0xAD, 0x00), + (0xAE, 0x00), + (0xAF, 0x00), + (0xB0, 0x48), + (0xB1, 0x00), + (0xB2, 0x08), + (0xB3, 0x02), + (0xB4, 0xE0), + (0xB5, 0x00), + (0xB6, 0x00), + (0xB7, 0x00), + (0xB8, 0x48), + (0xB9, 0x00), + (0xBA, 0x0A), + (0xBB, 0x02), + (0xBC, 0xE2), + (0xBD, 0x00), + (0xBE, 0x00), + (0xBF, 0x00), + (0xC0, 0x12), + (0xC1, 0xAA), + (0xC2, 0x65), + (0xC3, 0x74), + (0xC4, 0x47), + (0xC5, 0x56), + (0xC6, 0x00), + (0xC7, 0x88), + (0xC8, 0x99), + (0xC9, 0x33), + (0xD0, 0x21), + (0xD1, 0xAA), + (0xD2, 0x65), + (0xD3, 0x74), + (0xD4, 0x47), + (0xD5, 0x56), + (0xD6, 0x00), + (0xD7, 0x88), + (0xD8, 0x99), + (0xD9, 0x33), + (0xF3, 0x01), + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + (0xA0, 0x0B), + (0xA3, 0x2A), + (0xA5, 0xC3), + ), +) + +models = {} diff --git a/esphome/components/mipi_spi/models/lanbon.py b/esphome/components/mipi_spi/models/lanbon.py new file mode 100644 index 0000000000..6f9aa58674 --- /dev/null +++ b/esphome/components/mipi_spi/models/lanbon.py @@ -0,0 +1,15 @@ +from .ili import ST7789V + +ST7789V.extend( + "LANBON-L8", + width=240, + height=320, + mirror_x=True, + mirror_y=True, + data_rate="80MHz", + cs_pin=22, + dc_pin=21, + reset_pin=18, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/lilygo.py b/esphome/components/mipi_spi/models/lilygo.py new file mode 100644 index 0000000000..dd6f9c02f7 --- /dev/null +++ b/esphome/components/mipi_spi/models/lilygo.py @@ -0,0 +1,60 @@ +from esphome.components.spi import TYPE_OCTAL + +from .. import MODE_BGR +from .ili import ST7789V, ST7796 + +ST7789V.extend( + "T-EMBED", + width=170, + height=320, + offset_width=35, + color_order=MODE_BGR, + invert_colors=True, + draw_rounding=1, + cs_pin=10, + dc_pin=13, + reset_pin=9, + data_rate="80MHz", +) + +ST7789V.extend( + "T-DISPLAY", + height=240, + width=135, + offset_width=52, + offset_height=40, + draw_rounding=1, + cs_pin=5, + dc_pin=16, + invert_colors=True, +) +ST7789V.extend( + "T-DISPLAY-S3", + height=320, + width=170, + offset_width=35, + color_order=MODE_BGR, + invert_colors=True, + draw_rounding=1, + dc_pin=7, + cs_pin=6, + reset_pin=5, + enable_pin=[9, 15], + data_rate="10MHz", + bus_mode=TYPE_OCTAL, +) + +ST7796.extend( + "T-DISPLAY-S3-PRO", + width=222, + height=480, + offset_width=49, + draw_rounding=1, + cs_pin=39, + reset_pin=47, + dc_pin=9, + backlight_pin=48, + invert_colors=True, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py new file mode 100644 index 0000000000..6d14f56fc6 --- /dev/null +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -0,0 +1,139 @@ +from . import DriverChip +from .ili import ILI9488_A + +DriverChip( + "WAVESHARE-4-TFT", + width=320, + height=480, + invert_colors=True, + spi_16=True, + initsequence=( + ( + 0xF9, + 0x00, + 0x08, + ), + ( + 0xC0, + 0x19, + 0x1A, + ), + ( + 0xC1, + 0x45, + 0x00, + ), + ( + 0xC2, + 0x33, + ), + ( + 0xC5, + 0x00, + 0x28, + ), + ( + 0xB1, + 0xA0, + 0x11, + ), + ( + 0xB4, + 0x02, + ), + ( + 0xB6, + 0x00, + 0x42, + 0x3B, + ), + ( + 0xB7, + 0x07, + ), + ( + 0xE0, + 0x1F, + 0x25, + 0x22, + 0x0B, + 0x06, + 0x0A, + 0x4E, + 0xC6, + 0x39, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ), + ( + 0xE1, + 0x1F, + 0x3F, + 0x3F, + 0x0F, + 0x1F, + 0x0F, + 0x46, + 0x49, + 0x31, + 0x05, + 0x09, + 0x03, + 0x1C, + 0x1A, + 0x00, + ), + ( + 0xF1, + 0x36, + 0x04, + 0x00, + 0x3C, + 0x0F, + 0x0F, + 0xA4, + 0x02, + ), + ( + 0xF2, + 0x18, + 0xA3, + 0x12, + 0x02, + 0x32, + 0x12, + 0xFF, + 0x32, + 0x00, + ), + ( + 0xF4, + 0x40, + 0x00, + 0x08, + 0x91, + 0x04, + ), + ( + 0xF8, + 0x21, + 0x04, + ), + ), +) + +ILI9488_A.extend( + "PICO-RESTOUCH-LCD-3.5", + spi_16=True, + pixel_mode="16bit", + mirror_x=True, + dc_pin=33, + cs_pin=34, + reset_pin=40, + data_rate="20MHz", + invert_colors=True, +) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 7cdffafdb5..f96d3da251 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -355,6 +355,12 @@ class SPIComponent : public Component { void setup() override; void dump_config() override; + size_t get_bus_width() const { + if (this->data_pins_.empty()) { + return 1; + } + return this->data_pins_.size(); + } protected: GPIOPin *clk_pin_{nullptr}; diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml new file mode 100644 index 0000000000..e4b1e2b30c --- /dev/null +++ b/tests/components/mipi_spi/common.yaml @@ -0,0 +1,38 @@ +spi: + - id: spi_single + clk_pin: + number: ${clk_pin} + allow_other_uses: true + mosi_pin: + number: ${mosi_pin} + +display: + - platform: mipi_spi + spi_16: true + pixel_mode: 18bit + model: ili9488 + dc_pin: ${dc_pin} + cs_pin: ${cs_pin} + reset_pin: ${reset_pin} + data_rate: 20MHz + invert_colors: true + show_test_card: true + spi_mode: mode0 + draw_rounding: 8 + use_axis_flips: true + init_sequence: + - [0xd0, 1, 2, 3] + - delay 10ms + transform: + swap_xy: true + mirror_x: false + mirror_y: true + dimensions: + width: 100 + height: 200 + enable_pin: + - number: ${clk_pin} + allow_other_uses: true + - number: ${enable_pin} + bus_mode: single + diff --git a/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml new file mode 100644 index 0000000000..a28776798c --- /dev/null +++ b/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: ESP32-2432S028 diff --git a/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml new file mode 100644 index 0000000000..02b8f78d58 --- /dev/null +++ b/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: JC3248W535 diff --git a/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml new file mode 100644 index 0000000000..147d4833ac --- /dev/null +++ b/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml @@ -0,0 +1,19 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 36 + data_pins: + - number: 40 + - number: 41 + - number: 42 + - number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: JC3636W518 diff --git a/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8d96f31fd5 --- /dev/null +++ b/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: Pico-ResTouch-LCD-3.5 diff --git a/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml new file mode 100644 index 0000000000..98f6955bf3 --- /dev/null +++ b/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: S3BOX diff --git a/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml new file mode 100644 index 0000000000..11ad869d54 --- /dev/null +++ b/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: S3BOXLITE diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dc328f950c --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3-AMOLED-PLUS diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml new file mode 100644 index 0000000000..f0432270dc --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - number: 40 + - number: 41 + - number: 42 + - number: 43 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3-AMOLED diff --git a/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml new file mode 100644 index 0000000000..5cda38e096 --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 40 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3-PRO diff --git a/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml new file mode 100644 index 0000000000..144bde8366 --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml @@ -0,0 +1,37 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3 diff --git a/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml new file mode 100644 index 0000000000..39339b5ae2 --- /dev/null +++ b/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: T-DISPLAY diff --git a/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml new file mode 100644 index 0000000000..6c9edb25b3 --- /dev/null +++ b/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 40 + +display: + - platform: mipi_spi + model: T-EMBED diff --git a/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml new file mode 100644 index 0000000000..46eaedb7cb --- /dev/null +++ b/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: T4-S3 diff --git a/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml new file mode 100644 index 0000000000..3efb05ec89 --- /dev/null +++ b/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml @@ -0,0 +1,37 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 9 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + +display: + - platform: mipi_spi + model: WT32-SC01-PLUS diff --git a/tests/components/mipi_spi/test.esp32-ard.yaml b/tests/components/mipi_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..a5ef77dabc --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + dc_pin: GPIO14 + cs_pin: GPIO13 + enable_pin: GPIO19 + reset_pin: GPIO20 + +display: + - platform: mipi_spi + model: LANBON-L8 + +packages: + display: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-c3-ard.yaml b/tests/components/mipi_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c17748c569 --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + dc_pin: GPIO21 + cs_pin: GPIO18 + enable_pin: GPIO19 + reset_pin: GPIO20 + +<<: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-c3-idf.yaml b/tests/components/mipi_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c17748c569 --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + dc_pin: GPIO21 + cs_pin: GPIO18 + enable_pin: GPIO19 + reset_pin: GPIO20 + +<<: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-idf.yaml b/tests/components/mipi_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..653ccb4910 --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + dc_pin: GPIO21 + cs_pin: GPIO18 + enable_pin: GPIO19 + reset_pin: GPIO20 + +packages: + display: !include common.yaml + +display: + - platform: mipi_spi + model: m5core diff --git a/tests/components/mipi_spi/test.rp2040-ard.yaml b/tests/components/mipi_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5d7333853b --- /dev/null +++ b/tests/components/mipi_spi/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + dc_pin: GPIO14 + cs_pin: GPIO13 + enable_pin: GPIO19 + reset_pin: GPIO20 + +<<: !include common.yaml From 28e29efd98e780a4b8d2804e8c9d29f77c852a4e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 09:54:26 +1200 Subject: [PATCH 085/193] Bump version to 2025.6.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0f811aa870..8c2a1066bd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0-dev" +__version__ = "2025.6.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 06302441955a48dd12fab1df9c251c74aa27843b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 09:54:26 +1200 Subject: [PATCH 086/193] Bump version to 2025.5.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0f811aa870..0974a673ec 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0-dev" +__version__ = "2025.5.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From f28a373898924ee6e0ca1b3053887313a32b29b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 11:48:54 +1200 Subject: [PATCH 087/193] [media_player] Deprecate ``MEDIA_PLAYER_SCHEMA`` (#8784) --- .../i2s_audio/media_player/__init__.py | 19 ++++++----- esphome/components/media_player/__init__.py | 34 ++++++++++++++++++- .../speaker/media_player/__init__.py | 6 ++-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index bed25b011f..51001e9444 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -2,7 +2,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, media_player import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODE +from esphome.const import CONF_MODE from .. import ( CONF_I2S_AUDIO_ID, @@ -57,16 +57,17 @@ def validate_esp32_variant(config): CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "internal": media_player.MEDIA_PLAYER_SCHEMA.extend( + "internal": media_player.media_player_schema(I2SAudioMediaPlayer) + .extend( { - cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), } - ).extend(cv.COMPONENT_SCHEMA), - "external": media_player.MEDIA_PLAYER_SCHEMA.extend( + ) + .extend(cv.COMPONENT_SCHEMA), + "external": media_player.media_player_schema(I2SAudioMediaPlayer) + .extend( { - cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required( CONF_I2S_DOUT_PIN @@ -79,7 +80,8 @@ CONFIG_SCHEMA = cv.All( *I2C_COMM_FMT_OPTIONS, lower=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), }, key=CONF_DAC_TYPE, ), @@ -97,9 +99,8 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await media_player.new_media_player(config) await cg.register_component(var, config) - await media_player.register_media_player(var, config) await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 14fe1fdb6a..2f5fe0c03e 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -2,6 +2,8 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_ON_IDLE, CONF_ON_STATE, @@ -10,6 +12,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.coroutine import coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -103,7 +106,13 @@ async def register_media_player(var, config): await setup_media_player_core_(var, config) -MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( +async def new_media_player(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_media_player(var, config) + return var + + +_MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.Optional(CONF_ON_STATE): automation.validate_automation( { @@ -134,6 +143,29 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ) +def media_player_schema( + class_: MockObjClass, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _MEDIA_PLAYER_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) +MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) + + MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 35d763b1f8..cedafe214d 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -271,9 +271,8 @@ PIPELINE_SCHEMA = cv.Schema( ) CONFIG_SCHEMA = cv.All( - media_player.MEDIA_PLAYER_SCHEMA.extend( + media_player.media_player_schema(SpeakerMediaPlayer).extend( { - cv.GenerateID(): cv.declare_id(SpeakerMediaPlayer), cv.Required(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( @@ -343,9 +342,8 @@ async def to_code(config): # Allocate wifi buffers in PSRAM esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = cg.new_Pvariable(config[CONF_ID]) + var = await media_player.new_media_player(config) await cg.register_component(var, config) - await media_player.register_media_player(var, config) cg.add_define("USE_OTA_STATE_CALLBACK") From a835ab48bc9e4f6ca5a909ae46eb9f2563130bc3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 13:25:21 +1200 Subject: [PATCH 088/193] [schema] Get component name if available for deprecation warning (#8785) --- esphome/config_validation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 88a805591d..2eabcc8568 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2077,14 +2077,20 @@ def rename_key(old_key, new_key): # Remove before 2025.11.0 def deprecated_schema_constant(entity_type: str): def validator(config): + type: str = "unknown" + if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): + type = str(id.type).split("::", maxsplit=1)[0] _LOGGER.warning( "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " "Please use `%s.%s_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it.", + "If you are seeing this, report an issue to the external_component author and ask them to update it. " + "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " + "Component using this schema: %s", entity_type, entity_type.upper(), entity_type, entity_type, + type, ) return config From 42c355e6d77b42813875042363d5bb278e7516d0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 13:30:11 +1200 Subject: [PATCH 089/193] [fan] Update components to use ``fan_schema(...)`` (#8786) --- esphome/components/bedjet/fan/__init__.py | 13 ++------ esphome/components/binary/fan/__init__.py | 29 ++++++++--------- esphome/components/copy/fan/__init__.py | 20 ++++++------ esphome/components/hbridge/fan/__init__.py | 36 +++++++++++---------- esphome/components/speed/fan/__init__.py | 33 ++++++++++--------- esphome/components/template/fan/__init__.py | 26 ++++++++------- 6 files changed, 76 insertions(+), 81 deletions(-) diff --git a/esphome/components/bedjet/fan/__init__.py b/esphome/components/bedjet/fan/__init__.py index fdf0636153..a4a611fefc 100644 --- a/esphome/components/bedjet/fan/__init__.py +++ b/esphome/components/bedjet/fan/__init__.py @@ -1,31 +1,22 @@ -import logging - import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_ID from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child -_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["bedjet"] BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) CONFIG_SCHEMA = ( - fan.FAN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BedJetFan), - } - ) + fan.fan_schema(BedJetFan) .extend(cv.polling_component_schema("60s")) .extend(BEDJET_CLIENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) await register_bedjet_child(var, config) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index a504ef642c..dadcf52372 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,31 +1,28 @@ import esphome.codegen as cg from esphome.components import fan, output import esphome.config_validation as cv -from esphome.const import ( - CONF_DIRECTION_OUTPUT, - CONF_OSCILLATION_OUTPUT, - CONF_OUTPUT, - CONF_OUTPUT_ID, -) +from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT from .. import binary_ns BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(BinaryFan) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py index 04872fb029..a208e5f80a 100644 --- a/esphome/components/copy/fan/__init__.py +++ b/esphome/components/copy/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID +from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from from .. import copy_ns @@ -9,12 +9,15 @@ from .. import copy_ns CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyFan), - cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(CopyFan) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), @@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await fan.register_fan(var, config) + var = await fan.new_fan(config) await cg.register_component(var, config) source = await cg.get_variable(config[CONF_SOURCE_ID]) diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 4309a64359..31a20a8981 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -30,25 +30,28 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), - cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), - cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), - cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( - DECAY_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), - cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(HBridgeFan) + .extend( + { + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) @automation.register_action( "fan.hbridge.brake", BrakeAction, - maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), + maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), ) async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -56,13 +59,12 @@ async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): async def to_code(config): - var = cg.new_Pvariable( - config[CONF_ID], + var = await fan.new_fan( + config, config[CONF_SPEED_COUNT], config[CONF_DECAY_MODE], ) await cg.register_component(var, config) - await fan.register_fan(var, config) pin_a_ = await cg.get_variable(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a_)) pin_b_ = await cg.get_variable(config[CONF_PIN_B]) diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index fe43ac6a3a..3c495f3160 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_OUTPUT_ID, CONF_PRESET_MODES, CONF_SPEED, CONF_SPEED_COUNT, @@ -16,25 +15,27 @@ from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_SPEED): cv.invalid( - "Configuring individual speeds is deprecated." - ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(SpeedFan) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_SPEED): cv.invalid( + "Configuring individual speeds is deprecated." + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT]) + var = await fan.new_fan(config, config[CONF_SPEED_COUNT]) await cg.register_component(var, config) - await fan.register_fan(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py index c885866d40..72b20e1efe 100644 --- a/esphome/components/template/fan/__init__.py +++ b/esphome/components/template/fan/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import fan from esphome.components.fan import validate_preset_modes import esphome.config_validation as cv -from esphome.const import CONF_OUTPUT_ID, CONF_PRESET_MODES, CONF_SPEED_COUNT +from esphome.const import CONF_PRESET_MODES, CONF_SPEED_COUNT from .. import template_ns @@ -13,21 +13,23 @@ TemplateFan = template_ns.class_("TemplateFan", cg.Component, fan.Fan) CONF_HAS_DIRECTION = "has_direction" CONF_HAS_OSCILLATING = "has_oscillating" -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TemplateFan), - cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, - cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, - cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(TemplateFan) + .extend( + { + cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, + cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) cg.add(var.set_has_direction(config[CONF_HAS_DIRECTION])) cg.add(var.set_has_oscillating(config[CONF_HAS_OSCILLATING])) From 5570a788fd2d846e14c8518058298c173f28f7e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 21:23:32 -0500 Subject: [PATCH 090/193] Bump aioesphomeapi from 30.2.0 to 31.0.0 (#8779) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9547cd0ef0..985b9bf519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250415.0 -aioesphomeapi==30.2.0 +aioesphomeapi==31.0.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import From ddb986b4fa36ff2931c8848460b59b8d7f6169b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 May 2025 23:34:33 -0500 Subject: [PATCH 091/193] Improve batching of BLE advertisements for better airtime efficiency (#8778) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 77 ++++++++++++++----- .../bluetooth_proxy/bluetooth_proxy.h | 1 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 4 +- .../esp32_ble_tracker/esp32_ble_tracker.h | 2 +- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 9c8bd4009f..915d2882d3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -51,35 +51,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return true; } +static constexpr size_t FLUSH_BATCH_SIZE = 8; +static std::vector &get_batch_buffer() { + static std::vector batch_buffer; + return batch_buffer; +} + bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) return false; - api::BluetoothLERawAdvertisementsResponse resp; - // Pre-allocate the advertisements vector to avoid reallocations - resp.advertisements.reserve(count); + // Get the batch buffer reference + auto &batch_buffer = get_batch_buffer(); + // Reserve additional capacity if needed + size_t new_size = batch_buffer.size() + count; + if (batch_buffer.capacity() < new_size) { + batch_buffer.reserve(new_size); + } + + // Add new advertisements to the batch buffer for (size_t i = 0; i < count; i++) { auto &result = advertisements[i]; - api::BluetoothLERawAdvertisement adv; + uint8_t length = result.adv_data_len + result.scan_rsp_len; + + batch_buffer.emplace_back(); + auto &adv = batch_buffer.back(); adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.rssi = result.rssi; adv.address_type = result.ble_addr_type; + adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); - uint8_t length = result.adv_data_len + result.scan_rsp_len; - adv.data.reserve(length); - // Use a bulk insert instead of individual push_backs - adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]); - - resp.advertisements.push_back(std::move(adv)); - - ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], + ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); } - ESP_LOGV(TAG, "Proxying %d packets", count); - this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); + + // Only send if we've accumulated a good batch size to maximize batching efficiency + // https://github.com/esphome/backlog/issues/21 + if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { + this->flush_pending_advertisements(); + } + return true; } + +void BluetoothProxy::flush_pending_advertisements() { + auto &batch_buffer = get_batch_buffer(); + if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) + return; + + api::BluetoothLERawAdvertisementsResponse resp; + resp.advertisements.swap(batch_buffer); + this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); +} + void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -91,28 +116,28 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi // Pre-allocate vectors based on known sizes auto service_uuids = device.get_service_uuids(); resp.service_uuids.reserve(service_uuids.size()); - for (auto uuid : service_uuids) { - resp.service_uuids.push_back(uuid.to_string()); + for (auto &uuid : service_uuids) { + resp.service_uuids.emplace_back(uuid.to_string()); } // Pre-allocate service data vector auto service_datas = device.get_service_datas(); resp.service_data.reserve(service_datas.size()); for (auto &data : service_datas) { - api::BluetoothServiceData service_data; + resp.service_data.emplace_back(); + auto &service_data = resp.service_data.back(); service_data.uuid = data.uuid.to_string(); service_data.data.assign(data.data.begin(), data.data.end()); - resp.service_data.push_back(std::move(service_data)); } // Pre-allocate manufacturer data vector auto manufacturer_datas = device.get_manufacturer_datas(); resp.manufacturer_data.reserve(manufacturer_datas.size()); for (auto &data : manufacturer_datas) { - api::BluetoothServiceData manufacturer_data; + resp.manufacturer_data.emplace_back(); + auto &manufacturer_data = resp.manufacturer_data.back(); manufacturer_data.uuid = data.uuid.to_string(); manufacturer_data.data.assign(data.data.begin(), data.data.end()); - resp.manufacturer_data.push_back(std::move(manufacturer_data)); } this->api_connection_->send_bluetooth_le_advertisement(resp); @@ -148,6 +173,18 @@ void BluetoothProxy::loop() { } return; } + + // Flush any pending BLE advertisements that have been accumulated but not yet sent + if (this->raw_advertisements_) { + static uint32_t last_flush_time = 0; + uint32_t now = millis(); + + // Flush accumulated advertisements every 100ms + if (now - last_flush_time >= 100) { + this->flush_pending_advertisements(); + last_flush_time = now; + } + } for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { connection->send_service_ = DONE_SENDING_SERVICES; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index de24165fe8..f75e73e796 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -56,6 +56,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void dump_config() override; void setup() override; void loop() override; + void flush_pending_advertisements(); esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index be45b177ff..1a6071c9fe 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -122,7 +122,7 @@ void ESP32BLETracker::loop() { if (this->scanner_state_ == ScannerState::RUNNING && this->scan_result_index_ && // if it looks like we have a scan result we will take the lock - xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { + xSemaphoreTake(this->scan_result_lock_, 0)) { uint32_t index = this->scan_result_index_; if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); @@ -447,7 +447,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - if (xSemaphoreTake(this->scan_result_lock_, 0L)) { + if (xSemaphoreTake(this->scan_result_lock_, 0)) { if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { this->scan_result_buffer_[this->scan_result_index_++] = param; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 2e45d9602c..eea73a7d26 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -290,7 +290,7 @@ class ESP32BLETracker : public Component, #ifdef USE_PSRAM const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; #else - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16; + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; #endif // USE_PSRAM esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; From a12bd78ceb0ebcd2c8af978946b7dddcb102a9c2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 16:35:30 +1200 Subject: [PATCH 092/193] Fix release to pypi (#8789) --- .github/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88704953ce..41e9186987 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,16 +56,14 @@ jobs: uses: actions/setup-python@v5.6.0 with: python-version: "3.x" - - name: Set up python environment - env: - ESPHOME_NO_VENV: 1 - run: script/setup - name: Build run: |- pip3 install build python3 -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.12.4 + with: + skip-existing: true deploy-docker: name: Build ESPHome ${{ matrix.platform.arch }} From de27ce79dc4ced31dd787c787c6e91dc164abb17 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 17:36:21 +1200 Subject: [PATCH 093/193] [climate] Update components to use ``climate_schema(...)`` (#8788) --- esphome/components/anova/climate.py | 9 +-- esphome/components/ballu/climate.py | 11 +-- esphome/components/bang_bang/climate.py | 11 ++- esphome/components/bedjet/climate/__init__.py | 11 +-- esphome/components/climate_ir/__init__.py | 74 ++++++++++++++----- esphome/components/climate_ir_lg/climate.py | 7 +- esphome/components/coolix/climate.py | 11 +-- esphome/components/daikin/climate.py | 11 +-- esphome/components/daikin_arc/climate.py | 9 +-- esphome/components/daikin_brc/climate.py | 8 +- esphome/components/delonghi/climate.py | 11 +-- esphome/components/emmeti/climate.py | 11 +-- esphome/components/fujitsu_general/climate.py | 11 +-- esphome/components/gree/climate.py | 9 +-- esphome/components/haier/climate.py | 71 +++++++++--------- esphome/components/heatpumpir/climate.py | 9 +-- esphome/components/hitachi_ac344/climate.py | 11 +-- esphome/components/hitachi_ac424/climate.py | 11 +-- esphome/components/midea/climate.py | 7 +- esphome/components/midea_ir/climate.py | 8 +- esphome/components/mitsubishi/climate.py | 7 +- esphome/components/noblex/climate.py | 11 +-- esphome/components/pid/climate.py | 6 +- esphome/components/tcl112/climate.py | 11 +-- esphome/components/thermostat/climate.py | 10 +-- esphome/components/toshiba/climate.py | 8 +- esphome/components/tuya/climate/__init__.py | 11 ++- .../uponor_smatrix/climate/__init__.py | 13 +--- esphome/components/whirlpool/climate.py | 8 +- esphome/components/whynter/climate.py | 8 +- esphome/components/yashima/climate.py | 12 +-- esphome/components/zhlt01/climate.py | 9 +-- 32 files changed, 180 insertions(+), 255 deletions(-) diff --git a/esphome/components/anova/climate.py b/esphome/components/anova/climate.py index 052296294b..e1fd38fddc 100644 --- a/esphome/components/anova/climate.py +++ b/esphome/components/anova/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import ble_client, climate import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT +from esphome.const import CONF_UNIT_OF_MEASUREMENT UNITS = { "f": "f", @@ -17,9 +17,9 @@ Anova = anova_ns.class_( ) CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(Anova) + .extend( { - cv.GenerateID(): cv.declare_id(Anova), cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), } ) @@ -29,8 +29,7 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) diff --git a/esphome/components/ballu/climate.py b/esphome/components/ballu/climate.py index 416fa250cc..e35a1d244d 100644 --- a/esphome/components/ballu/climate.py +++ b/esphome/components/ballu/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@bazuchan"] @@ -9,13 +7,8 @@ CODEOWNERS = ["@bazuchan"] ballu_ns = cg.esphome_ns.namespace("ballu") BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BalluClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 6511270f60..bfdb12278f 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, CONF_HUMIDITY_SENSOR, - CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR, ) @@ -19,9 +18,9 @@ BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Com BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig") CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(BangBangClimate) + .extend( { - cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, @@ -36,15 +35,15 @@ CONFIG_SCHEMA = cv.All( } ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION), ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) diff --git a/esphome/components/bedjet/climate/__init__.py b/esphome/components/bedjet/climate/__init__.py index 7ba3e439b2..e9c5510256 100644 --- a/esphome/components/bedjet/climate/__init__.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -1,11 +1,8 @@ -import logging - import esphome.codegen as cg from esphome.components import ble_client, climate import esphome.config_validation as cv from esphome.const import ( CONF_HEAT_MODE, - CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TEMPERATURE_SOURCE, CONF_TIME_ID, @@ -13,7 +10,6 @@ from esphome.const import ( from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child -_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["bedjet"] @@ -30,9 +26,9 @@ BEDJET_TEMPERATURE_SOURCES = { } CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(BedJetClimate) + .extend( { - cv.GenerateID(): cv.declare_id(BedJetClimate), cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( BEDJET_HEAT_MODES, lower=True ), @@ -63,9 +59,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) await register_bedjet_child(var, config) cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index d8be61397e..32b614e933 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,7 +1,13 @@ +import logging + +from esphome import core import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.cpp_generator import MockObjClass + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["remote_transmitter"] AUTO_LOAD = ["sensor", "remote_base"] @@ -16,30 +22,58 @@ ClimateIR = climate_ir_ns.class_( remote_base.RemoteTransmittable, ) -CLIMATE_IR_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( + +def climate_ir_schema( + class_: MockObjClass, +) -> cv.Schema: + return ( + climate.climate_schema(class_) + .extend( + { + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) + ) + + +def climare_ir_with_receiver_schema( + class_: MockObjClass, +) -> cv.Schema: + return climate_ir_schema(class_).extend( { - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( + remote_base.RemoteReceiverBase + ), } ) - .extend(cv.COMPONENT_SCHEMA) - .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) -) -CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( - { - cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( - remote_base.RemoteReceiverBase - ), - } -) + +# Remove before 2025.11.0 +def deprecated_schema_constant(config): + type: str = "unknown" + if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): + type = str(id.type).split("::", maxsplit=1)[0] + _LOGGER.warning( + "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " + "Please use `climate_ir.climare_ir_with_receiver_schema(...)` instead. " + "If you are seeing this, report an issue to the external_component author and ask them to update it. " + "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " + "Component using this schema: %s", + type, + ) + return config + + +CLIMATE_IR_WITH_RECEIVER_SCHEMA = climare_ir_with_receiver_schema(ClimateIR) +CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) async def register_climate_ir(var, config): await cg.register_component(var, config) - await climate.register_climate(var, config) await remote_base.register_transmittable(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) @@ -48,3 +82,9 @@ async def register_climate_ir(var, config): if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) + + +async def new_climate_ir(config, *args): + var = await climate.new_climate(config, *args) + await register_climate_ir(var, config) + return var diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py index 76d4c00baf..de824bfe5c 100644 --- a/esphome/components/climate_ir_lg/climate.py +++ b/esphome/components/climate_ir_lg/climate.py @@ -1,7 +1,6 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] @@ -14,9 +13,8 @@ CONF_BIT_HIGH = "bit_high" CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ZERO_LOW = "bit_zero_low" -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(LgIrClimate).extend( { - cv.GenerateID(): cv.declare_id(LgIrClimate), cv.Optional( CONF_HEADER_HIGH, default="8000us" ): cv.positive_time_period_microseconds, @@ -37,8 +35,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_header_high(config[CONF_HEADER_HIGH])) cg.add(var.set_header_low(config[CONF_HEADER_LOW])) diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 339e7de906..b280544a5c 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@glmnet"] @@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"] coolix_ns = cg.esphome_ns.namespace("coolix") CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CoolixClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(CoolixClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py index 3946513191..2cd44969c1 100644 --- a/esphome/components/daikin/climate.py +++ b/esphome/components/daikin/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] daikin_ns = cg.esphome_ns.namespace("daikin") DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(DaikinClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/daikin_arc/climate.py b/esphome/components/daikin_arc/climate.py index 967d080c24..8f6b07315d 100644 --- a/esphome/components/daikin_arc/climate.py +++ b/esphome/components/daikin_arc/climate.py @@ -1,18 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(DaikinArcClimate)} -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinArcClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py index aacac408ca..1000784380 100644 --- a/esphome/components/daikin_brc/climate.py +++ b/esphome/components/daikin_brc/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT +from esphome.const import CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir"] @@ -9,15 +9,13 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinBrcClimate).extend( { - cv.GenerateID(): cv.declare_id(DaikinBrcClimate), cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py index 0d3bb76c98..ff878b4ff7 100644 --- a/esphome/components/delonghi/climate.py +++ b/esphome/components/delonghi/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] delonghi_ns = cg.esphome_ns.namespace("delonghi") DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(DelonghiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DelonghiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/emmeti/climate.py b/esphome/components/emmeti/climate.py index b925f4b61e..042f1af64b 100644 --- a/esphome/components/emmeti/climate.py +++ b/esphome/components/emmeti/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID CODEOWNERS = ["@E440QF"] AUTO_LOAD = ["climate_ir"] @@ -9,13 +7,8 @@ AUTO_LOAD = ["climate_ir"] emmeti_ns = cg.esphome_ns.namespace("emmeti") EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EmmetiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(EmmetiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py index 6d2e46512e..0f028d0af3 100644 --- a/esphome/components/fujitsu_general/climate.py +++ b/esphome/components/fujitsu_general/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] @@ -10,13 +8,8 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_( "FujitsuGeneralClimate", climate_ir.ClimateIR ) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(FujitsuGeneralClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py index 389c9fb3c7..947ef9bb97 100644 --- a/esphome/components/gree/climate.py +++ b/esphome/components/gree/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.const import CONF_MODEL CODEOWNERS = ["@orestismers"] @@ -21,16 +21,13 @@ MODELS = { "yag": Model.GREE_YAG, } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(GreeClimate).extend( { - cv.GenerateID(): cv.declare_id(GreeClimate), cv.Required(CONF_MODEL): cv.enum(MODELS), } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) - - await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index f77d624649..0393c263d4 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -30,6 +30,7 @@ from esphome.const import ( CONF_VISUAL, CONF_WIFI, ) +from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) @@ -185,42 +186,46 @@ def validate_visual(config): return config -BASE_CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( - { - cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( - cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) - ), - cv.Optional( - CONF_SUPPORTED_SWING_MODES, - default=[ - "VERTICAL", - "HORIZONTAL", - "BOTH", - ], - ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), - cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, - cv.Optional(CONF_DISPLAY): cv.boolean, - cv.Optional( - CONF_ANSWER_TIMEOUT, - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusMessageTrigger), - } - ), - } +def _base_config_schema(class_: MockObjClass) -> cv.Schema: + return ( + climate.climate_schema(class_) + .extend( + { + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) + ), + cv.Optional( + CONF_SUPPORTED_SWING_MODES, + default=[ + "VERTICAL", + "HORIZONTAL", + "BOTH", + ], + ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, + cv.Optional(CONF_DISPLAY): cv.boolean, + cv.Optional( + CONF_ANSWER_TIMEOUT, + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + StatusMessageTrigger + ), + } + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) ) - .extend(uart.UART_DEVICE_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) -) + CONFIG_SCHEMA = cv.All( cv.typed_schema( { - PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( + PROTOCOL_SMARTAIR2: _base_config_schema(Smartair2Climate).extend( { - cv.GenerateID(): cv.declare_id(Smartair2Climate), cv.Optional( CONF_ALTERNATIVE_SWING_CONTROL, default=False ): cv.boolean, @@ -232,9 +237,8 @@ CONFIG_SCHEMA = cv.All( ), } ), - PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( + PROTOCOL_HON: _base_config_schema(HonClimate).extend( { - cv.GenerateID(): cv.declare_id(HonClimate), cv.Optional( CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" ): cv.ensure_list( @@ -464,10 +468,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): cg.add(haier_ns.init_haier_protocol_logging()) - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) await uart.register_uart_device(var, config) - await climate.register_climate(var, config) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) if CONF_CONTROL_METHOD in config: diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 612b0d6123..21b0168615 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -2,7 +2,6 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_MAX_TEMPERATURE, CONF_MIN_TEMPERATURE, CONF_PROTOCOL, @@ -98,9 +97,8 @@ VERTICAL_DIRECTIONS = { } CONFIG_SCHEMA = cv.All( - climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + climate_ir.climare_ir_with_receiver_schema(HeatpumpIRClimate).extend( { - cv.GenerateID(): cv.declare_id(HeatpumpIRClimate), cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), cv.Required(CONF_VERTICAL_DEFAULT): cv.enum(VERTICAL_DIRECTIONS), @@ -112,8 +110,8 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def to_code(config): + var = await climate_ir.new_climate_ir(config) if CONF_VISUAL not in config: config[CONF_VISUAL] = {} visual = config[CONF_VISUAL] @@ -121,7 +119,6 @@ def to_code(config): visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE] if CONF_MIN_TEMPERATURE not in visual: visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE] - yield climate_ir.register_climate_ir(var, config) cg.add(var.set_protocol(config[CONF_PROTOCOL])) cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py index 0988d63995..4fa2d54fbb 100644 --- a/esphome/components/hitachi_ac344/climate.py +++ b/esphome/components/hitachi_ac344/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HitachiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/hitachi_ac424/climate.py b/esphome/components/hitachi_ac424/climate.py index 74f3c2fa14..4b20147922 100644 --- a/esphome/components/hitachi_ac424/climate.py +++ b/esphome/components/hitachi_ac424/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HitachiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 1d3cac66ba..b08a47afa9 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -104,9 +104,9 @@ validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(AirConditioner) + .extend( { - cv.GenerateID(): cv.declare_id(AirConditioner), cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), @@ -259,10 +259,9 @@ async def power_inv_to_code(var, config, args): async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) await uart.register_uart_device(var, config) - await climate.register_climate(var, config) cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 21fa5f4f56..5c9256b5e4 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT +from esphome.const import CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir", "coolix"] CODEOWNERS = ["@dudanov"] @@ -10,15 +10,13 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir") MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MideaIR).extend( { - cv.GenerateID(): cv.declare_id(MideaIR), cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py index 23f8ed21fa..5784d3ee8a 100644 --- a/esphome/components/mitsubishi/climate.py +++ b/esphome/components/mitsubishi/climate.py @@ -1,7 +1,6 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID CODEOWNERS = ["@RubyBailey"] AUTO_LOAD = ["climate_ir"] @@ -44,9 +43,8 @@ VERTICAL_DIRECTIONS = { } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MitsubishiClimate).extend( { - cv.GenerateID(): cv.declare_id(MitsubishiClimate), cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE), cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean, cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean, @@ -61,8 +59,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE])) cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY])) diff --git a/esphome/components/noblex/climate.py b/esphome/components/noblex/climate.py index 7f4e8e6488..d619265d01 100644 --- a/esphome/components/noblex/climate.py +++ b/esphome/components/noblex/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] noblex_ns = cg.esphome_ns.namespace("noblex") NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NoblexClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(NoblexClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index aab7ee5c00..5919d2cac8 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -41,9 +41,8 @@ CONF_KI_MULTIPLIER = "ki_multiplier" CONF_KD_MULTIPLIER = "kd_multiplier" CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(PIDClimate).extend( { - cv.GenerateID(): cv.declare_id(PIDClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, @@ -80,9 +79,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 9cd193f5c7..9864113a52 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@glmnet"] @@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"] tcl112_ns = cg.esphome_ns.namespace("tcl112") Tcl112Climate = tcl112_ns.class_("Tcl112Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(Tcl112Climate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Tcl112Climate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 638aad7c06..0314d877a3 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -516,9 +516,9 @@ def validate_thermostat(config): CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(ThermostatClimate) + .extend( { - cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), @@ -631,7 +631,8 @@ CONFIG_SCHEMA = cv.All( single=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( CONF_COOL_ACTION, CONF_DRY_ACTION, CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION ), @@ -640,9 +641,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 54582b78a9..40112fc460 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.const import CONF_MODEL AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@kbx81"] @@ -16,15 +16,13 @@ MODELS = { "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ToshibaClimate).extend( { - cv.GenerateID(): cv.declare_id(ToshibaClimate), cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True), } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 371c599ef7..4dbdf07651 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -4,7 +4,6 @@ from esphome.components import climate import esphome.config_validation as cv from esphome.const import ( CONF_FAN_MODE, - CONF_ID, CONF_PRESET, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, @@ -151,9 +150,9 @@ SWING_MODES = cv.Schema( ) CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(TuyaClimate) + .extend( { - cv.GenerateID(): cv.declare_id(TuyaClimate), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean, @@ -186,7 +185,8 @@ CONFIG_SCHEMA = cv.All( "'eco_temperature' has been moved inside of the 'eco' config block under 'preset' as 'temperature'" ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), validate_temperature_multipliers, validate_cooling_values, @@ -194,9 +194,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/uponor_smatrix/climate/__init__.py b/esphome/components/uponor_smatrix/climate/__init__.py index 5aeb521fb1..47495fde9a 100644 --- a/esphome/components/uponor_smatrix/climate/__init__.py +++ b/esphome/components/uponor_smatrix/climate/__init__.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate -import esphome.config_validation as cv -from esphome.const import CONF_ID from .. import ( UPONOR_SMATRIX_DEVICE_SCHEMA, @@ -19,15 +17,12 @@ UponorSmatrixClimate = uponor_smatrix_ns.class_( UponorSmatrixDevice, ) -CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UponorSmatrixClimate), - } -).extend(UPONOR_SMATRIX_DEVICE_SCHEMA) +CONFIG_SCHEMA = climate.climate_schema(UponorSmatrixClimate).extend( + UPONOR_SMATRIX_DEVICE_SCHEMA +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) await register_uponor_smatrix_device(var, config) diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py index 40c6053349..daee9e7fb7 100644 --- a/esphome/components/whirlpool/climate.py +++ b/esphome/components/whirlpool/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.const import CONF_MODEL AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@glmnet"] @@ -15,15 +15,13 @@ MODELS = { "DG11J1-91": Model.MODEL_DG11J1_91, } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(WhirlpoolClimate).extend( { - cv.GenerateID(): cv.declare_id(WhirlpoolClimate), cv.Optional(CONF_MODEL, default="DG11J1-3A"): cv.enum(MODELS, upper=True), } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py index ae21c64e9b..4a01c014c7 100644 --- a/esphome/components/whynter/climate.py +++ b/esphome/components/whynter/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT +from esphome.const import CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir"] @@ -9,15 +9,13 @@ whynter_ns = cg.esphome_ns.namespace("whynter") Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Whynter).extend( { - cv.GenerateID(): cv.declare_id(Whynter), cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py index eb68d3b6e7..d7386eb6a3 100644 --- a/esphome/components/yashima/climate.py +++ b/esphome/components/yashima/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import climate, remote_transmitter, sensor from esphome.components.remote_base import CONF_TRANSMITTER_ID import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT AUTO_LOAD = ["sensor"] @@ -10,9 +10,9 @@ yashima_ns = cg.esphome_ns.namespace("yashima") YashimaClimate = yashima_ns.class_("YashimaClimate", climate.Climate, cg.Component) CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(YashimaClimate) + .extend( { - cv.GenerateID(): cv.declare_id(YashimaClimate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( remote_transmitter.RemoteTransmitterComponent ), @@ -20,14 +20,14 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), } - ).extend(cv.COMPONENT_SCHEMA) + ) + .extend(cv.COMPONENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) diff --git a/esphome/components/zhlt01/climate.py b/esphome/components/zhlt01/climate.py index fc01107e1d..d5098ab42c 100644 --- a/esphome/components/zhlt01/climate.py +++ b/esphome/components/zhlt01/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@cfeenstra1024"] @@ -9,11 +7,8 @@ CODEOWNERS = ["@cfeenstra1024"] zhlt01_ns = cg.esphome_ns.namespace("zhlt01") ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(ZHLT01Climate)} -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ZHLT01Climate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) From 191afd3e6926300342f178d08c7035ed739f9072 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 19:08:16 +1200 Subject: [PATCH 094/193] Bump esphome-dashboard to 20250514.0 (#8790) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 985b9bf519..328bc33543 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 -esphome-dashboard==20250415.0 +esphome-dashboard==20250514.0 aioesphomeapi==31.0.0 zeroconf==0.147.0 puremagic==1.29 From ea3112297916831478d94826a8042b0dc15eafb1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 11:48:54 +1200 Subject: [PATCH 095/193] [media_player] Deprecate ``MEDIA_PLAYER_SCHEMA`` (#8784) --- .../i2s_audio/media_player/__init__.py | 19 ++++++----- esphome/components/media_player/__init__.py | 34 ++++++++++++++++++- .../speaker/media_player/__init__.py | 6 ++-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index bed25b011f..51001e9444 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -2,7 +2,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, media_player import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODE +from esphome.const import CONF_MODE from .. import ( CONF_I2S_AUDIO_ID, @@ -57,16 +57,17 @@ def validate_esp32_variant(config): CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "internal": media_player.MEDIA_PLAYER_SCHEMA.extend( + "internal": media_player.media_player_schema(I2SAudioMediaPlayer) + .extend( { - cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), } - ).extend(cv.COMPONENT_SCHEMA), - "external": media_player.MEDIA_PLAYER_SCHEMA.extend( + ) + .extend(cv.COMPONENT_SCHEMA), + "external": media_player.media_player_schema(I2SAudioMediaPlayer) + .extend( { - cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required( CONF_I2S_DOUT_PIN @@ -79,7 +80,8 @@ CONFIG_SCHEMA = cv.All( *I2C_COMM_FMT_OPTIONS, lower=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), }, key=CONF_DAC_TYPE, ), @@ -97,9 +99,8 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await media_player.new_media_player(config) await cg.register_component(var, config) - await media_player.register_media_player(var, config) await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 14fe1fdb6a..2f5fe0c03e 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -2,6 +2,8 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_ON_IDLE, CONF_ON_STATE, @@ -10,6 +12,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.coroutine import coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -103,7 +106,13 @@ async def register_media_player(var, config): await setup_media_player_core_(var, config) -MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( +async def new_media_player(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_media_player(var, config) + return var + + +_MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.Optional(CONF_ON_STATE): automation.validate_automation( { @@ -134,6 +143,29 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ) +def media_player_schema( + class_: MockObjClass, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _MEDIA_PLAYER_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) +MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) + + MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 35d763b1f8..cedafe214d 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -271,9 +271,8 @@ PIPELINE_SCHEMA = cv.Schema( ) CONFIG_SCHEMA = cv.All( - media_player.MEDIA_PLAYER_SCHEMA.extend( + media_player.media_player_schema(SpeakerMediaPlayer).extend( { - cv.GenerateID(): cv.declare_id(SpeakerMediaPlayer), cv.Required(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( @@ -343,9 +342,8 @@ async def to_code(config): # Allocate wifi buffers in PSRAM esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = cg.new_Pvariable(config[CONF_ID]) + var = await media_player.new_media_player(config) await cg.register_component(var, config) - await media_player.register_media_player(var, config) cg.add_define("USE_OTA_STATE_CALLBACK") From c30ffd009876a1adaeee484b66784a8297dc9e08 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 13:25:21 +1200 Subject: [PATCH 096/193] [schema] Get component name if available for deprecation warning (#8785) --- esphome/config_validation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 88a805591d..2eabcc8568 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2077,14 +2077,20 @@ def rename_key(old_key, new_key): # Remove before 2025.11.0 def deprecated_schema_constant(entity_type: str): def validator(config): + type: str = "unknown" + if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): + type = str(id.type).split("::", maxsplit=1)[0] _LOGGER.warning( "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " "Please use `%s.%s_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it.", + "If you are seeing this, report an issue to the external_component author and ask them to update it. " + "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " + "Component using this schema: %s", entity_type, entity_type.upper(), entity_type, entity_type, + type, ) return config From 7d0262dd1a8e52b663e31b055d9eab19218ddea1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 13:30:11 +1200 Subject: [PATCH 097/193] [fan] Update components to use ``fan_schema(...)`` (#8786) --- esphome/components/bedjet/fan/__init__.py | 13 ++------ esphome/components/binary/fan/__init__.py | 29 ++++++++--------- esphome/components/copy/fan/__init__.py | 20 ++++++------ esphome/components/hbridge/fan/__init__.py | 36 +++++++++++---------- esphome/components/speed/fan/__init__.py | 33 ++++++++++--------- esphome/components/template/fan/__init__.py | 26 ++++++++------- 6 files changed, 76 insertions(+), 81 deletions(-) diff --git a/esphome/components/bedjet/fan/__init__.py b/esphome/components/bedjet/fan/__init__.py index fdf0636153..a4a611fefc 100644 --- a/esphome/components/bedjet/fan/__init__.py +++ b/esphome/components/bedjet/fan/__init__.py @@ -1,31 +1,22 @@ -import logging - import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_ID from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child -_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["bedjet"] BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) CONFIG_SCHEMA = ( - fan.FAN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BedJetFan), - } - ) + fan.fan_schema(BedJetFan) .extend(cv.polling_component_schema("60s")) .extend(BEDJET_CLIENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) await register_bedjet_child(var, config) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index a504ef642c..dadcf52372 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,31 +1,28 @@ import esphome.codegen as cg from esphome.components import fan, output import esphome.config_validation as cv -from esphome.const import ( - CONF_DIRECTION_OUTPUT, - CONF_OSCILLATION_OUTPUT, - CONF_OUTPUT, - CONF_OUTPUT_ID, -) +from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT from .. import binary_ns BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(BinaryFan) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py index 04872fb029..a208e5f80a 100644 --- a/esphome/components/copy/fan/__init__.py +++ b/esphome/components/copy/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID +from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from from .. import copy_ns @@ -9,12 +9,15 @@ from .. import copy_ns CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyFan), - cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(CopyFan) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), @@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await fan.register_fan(var, config) + var = await fan.new_fan(config) await cg.register_component(var, config) source = await cg.get_variable(config[CONF_SOURCE_ID]) diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 4309a64359..31a20a8981 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -30,25 +30,28 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), - cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), - cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), - cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( - DECAY_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), - cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(HBridgeFan) + .extend( + { + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) @automation.register_action( "fan.hbridge.brake", BrakeAction, - maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), + maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), ) async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -56,13 +59,12 @@ async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): async def to_code(config): - var = cg.new_Pvariable( - config[CONF_ID], + var = await fan.new_fan( + config, config[CONF_SPEED_COUNT], config[CONF_DECAY_MODE], ) await cg.register_component(var, config) - await fan.register_fan(var, config) pin_a_ = await cg.get_variable(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a_)) pin_b_ = await cg.get_variable(config[CONF_PIN_B]) diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index fe43ac6a3a..3c495f3160 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_OUTPUT_ID, CONF_PRESET_MODES, CONF_SPEED, CONF_SPEED_COUNT, @@ -16,25 +15,27 @@ from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_SPEED): cv.invalid( - "Configuring individual speeds is deprecated." - ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(SpeedFan) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_SPEED): cv.invalid( + "Configuring individual speeds is deprecated." + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT]) + var = await fan.new_fan(config, config[CONF_SPEED_COUNT]) await cg.register_component(var, config) - await fan.register_fan(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py index c885866d40..72b20e1efe 100644 --- a/esphome/components/template/fan/__init__.py +++ b/esphome/components/template/fan/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import fan from esphome.components.fan import validate_preset_modes import esphome.config_validation as cv -from esphome.const import CONF_OUTPUT_ID, CONF_PRESET_MODES, CONF_SPEED_COUNT +from esphome.const import CONF_PRESET_MODES, CONF_SPEED_COUNT from .. import template_ns @@ -13,21 +13,23 @@ TemplateFan = template_ns.class_("TemplateFan", cg.Component, fan.Fan) CONF_HAS_DIRECTION = "has_direction" CONF_HAS_OSCILLATING = "has_oscillating" -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TemplateFan), - cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, - cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, - cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(TemplateFan) + .extend( + { + cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, + cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) cg.add(var.set_has_direction(config[CONF_HAS_DIRECTION])) cg.add(var.set_has_oscillating(config[CONF_HAS_OSCILLATING])) From 4f2643e6e9d4a34d8579f6211ab176af803fc648 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 May 2025 23:34:33 -0500 Subject: [PATCH 098/193] Improve batching of BLE advertisements for better airtime efficiency (#8778) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 77 ++++++++++++++----- .../bluetooth_proxy/bluetooth_proxy.h | 1 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 4 +- .../esp32_ble_tracker/esp32_ble_tracker.h | 2 +- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 9c8bd4009f..915d2882d3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -51,35 +51,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return true; } +static constexpr size_t FLUSH_BATCH_SIZE = 8; +static std::vector &get_batch_buffer() { + static std::vector batch_buffer; + return batch_buffer; +} + bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) return false; - api::BluetoothLERawAdvertisementsResponse resp; - // Pre-allocate the advertisements vector to avoid reallocations - resp.advertisements.reserve(count); + // Get the batch buffer reference + auto &batch_buffer = get_batch_buffer(); + // Reserve additional capacity if needed + size_t new_size = batch_buffer.size() + count; + if (batch_buffer.capacity() < new_size) { + batch_buffer.reserve(new_size); + } + + // Add new advertisements to the batch buffer for (size_t i = 0; i < count; i++) { auto &result = advertisements[i]; - api::BluetoothLERawAdvertisement adv; + uint8_t length = result.adv_data_len + result.scan_rsp_len; + + batch_buffer.emplace_back(); + auto &adv = batch_buffer.back(); adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.rssi = result.rssi; adv.address_type = result.ble_addr_type; + adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); - uint8_t length = result.adv_data_len + result.scan_rsp_len; - adv.data.reserve(length); - // Use a bulk insert instead of individual push_backs - adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]); - - resp.advertisements.push_back(std::move(adv)); - - ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], + ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); } - ESP_LOGV(TAG, "Proxying %d packets", count); - this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); + + // Only send if we've accumulated a good batch size to maximize batching efficiency + // https://github.com/esphome/backlog/issues/21 + if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { + this->flush_pending_advertisements(); + } + return true; } + +void BluetoothProxy::flush_pending_advertisements() { + auto &batch_buffer = get_batch_buffer(); + if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) + return; + + api::BluetoothLERawAdvertisementsResponse resp; + resp.advertisements.swap(batch_buffer); + this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); +} + void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -91,28 +116,28 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi // Pre-allocate vectors based on known sizes auto service_uuids = device.get_service_uuids(); resp.service_uuids.reserve(service_uuids.size()); - for (auto uuid : service_uuids) { - resp.service_uuids.push_back(uuid.to_string()); + for (auto &uuid : service_uuids) { + resp.service_uuids.emplace_back(uuid.to_string()); } // Pre-allocate service data vector auto service_datas = device.get_service_datas(); resp.service_data.reserve(service_datas.size()); for (auto &data : service_datas) { - api::BluetoothServiceData service_data; + resp.service_data.emplace_back(); + auto &service_data = resp.service_data.back(); service_data.uuid = data.uuid.to_string(); service_data.data.assign(data.data.begin(), data.data.end()); - resp.service_data.push_back(std::move(service_data)); } // Pre-allocate manufacturer data vector auto manufacturer_datas = device.get_manufacturer_datas(); resp.manufacturer_data.reserve(manufacturer_datas.size()); for (auto &data : manufacturer_datas) { - api::BluetoothServiceData manufacturer_data; + resp.manufacturer_data.emplace_back(); + auto &manufacturer_data = resp.manufacturer_data.back(); manufacturer_data.uuid = data.uuid.to_string(); manufacturer_data.data.assign(data.data.begin(), data.data.end()); - resp.manufacturer_data.push_back(std::move(manufacturer_data)); } this->api_connection_->send_bluetooth_le_advertisement(resp); @@ -148,6 +173,18 @@ void BluetoothProxy::loop() { } return; } + + // Flush any pending BLE advertisements that have been accumulated but not yet sent + if (this->raw_advertisements_) { + static uint32_t last_flush_time = 0; + uint32_t now = millis(); + + // Flush accumulated advertisements every 100ms + if (now - last_flush_time >= 100) { + this->flush_pending_advertisements(); + last_flush_time = now; + } + } for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { connection->send_service_ = DONE_SENDING_SERVICES; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index de24165fe8..f75e73e796 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -56,6 +56,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void dump_config() override; void setup() override; void loop() override; + void flush_pending_advertisements(); esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index be45b177ff..1a6071c9fe 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -122,7 +122,7 @@ void ESP32BLETracker::loop() { if (this->scanner_state_ == ScannerState::RUNNING && this->scan_result_index_ && // if it looks like we have a scan result we will take the lock - xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { + xSemaphoreTake(this->scan_result_lock_, 0)) { uint32_t index = this->scan_result_index_; if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); @@ -447,7 +447,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - if (xSemaphoreTake(this->scan_result_lock_, 0L)) { + if (xSemaphoreTake(this->scan_result_lock_, 0)) { if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { this->scan_result_buffer_[this->scan_result_index_++] = param; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 2e45d9602c..eea73a7d26 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -290,7 +290,7 @@ class ESP32BLETracker : public Component, #ifdef USE_PSRAM const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; #else - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16; + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; #endif // USE_PSRAM esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; From c050e8d0fbb05a4e8c06e3025d83ebc18606f7c0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 16:35:30 +1200 Subject: [PATCH 099/193] Fix release to pypi (#8789) --- .github/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88704953ce..41e9186987 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,16 +56,14 @@ jobs: uses: actions/setup-python@v5.6.0 with: python-version: "3.x" - - name: Set up python environment - env: - ESPHOME_NO_VENV: 1 - run: script/setup - name: Build run: |- pip3 install build python3 -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.12.4 + with: + skip-existing: true deploy-docker: name: Build ESPHome ${{ matrix.platform.arch }} From 7cb01bf8426b1bdc28f691ab85eda8d9c5d02fbb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 17:36:21 +1200 Subject: [PATCH 100/193] [climate] Update components to use ``climate_schema(...)`` (#8788) --- esphome/components/anova/climate.py | 9 +-- esphome/components/ballu/climate.py | 11 +-- esphome/components/bang_bang/climate.py | 11 ++- esphome/components/bedjet/climate/__init__.py | 11 +-- esphome/components/climate_ir/__init__.py | 74 ++++++++++++++----- esphome/components/climate_ir_lg/climate.py | 7 +- esphome/components/coolix/climate.py | 11 +-- esphome/components/daikin/climate.py | 11 +-- esphome/components/daikin_arc/climate.py | 9 +-- esphome/components/daikin_brc/climate.py | 8 +- esphome/components/delonghi/climate.py | 11 +-- esphome/components/emmeti/climate.py | 11 +-- esphome/components/fujitsu_general/climate.py | 11 +-- esphome/components/gree/climate.py | 9 +-- esphome/components/haier/climate.py | 71 +++++++++--------- esphome/components/heatpumpir/climate.py | 9 +-- esphome/components/hitachi_ac344/climate.py | 11 +-- esphome/components/hitachi_ac424/climate.py | 11 +-- esphome/components/midea/climate.py | 7 +- esphome/components/midea_ir/climate.py | 8 +- esphome/components/mitsubishi/climate.py | 7 +- esphome/components/noblex/climate.py | 11 +-- esphome/components/pid/climate.py | 6 +- esphome/components/tcl112/climate.py | 11 +-- esphome/components/thermostat/climate.py | 10 +-- esphome/components/toshiba/climate.py | 8 +- esphome/components/tuya/climate/__init__.py | 11 ++- .../uponor_smatrix/climate/__init__.py | 13 +--- esphome/components/whirlpool/climate.py | 8 +- esphome/components/whynter/climate.py | 8 +- esphome/components/yashima/climate.py | 12 +-- esphome/components/zhlt01/climate.py | 9 +-- 32 files changed, 180 insertions(+), 255 deletions(-) diff --git a/esphome/components/anova/climate.py b/esphome/components/anova/climate.py index 052296294b..e1fd38fddc 100644 --- a/esphome/components/anova/climate.py +++ b/esphome/components/anova/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import ble_client, climate import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT +from esphome.const import CONF_UNIT_OF_MEASUREMENT UNITS = { "f": "f", @@ -17,9 +17,9 @@ Anova = anova_ns.class_( ) CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(Anova) + .extend( { - cv.GenerateID(): cv.declare_id(Anova), cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), } ) @@ -29,8 +29,7 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) diff --git a/esphome/components/ballu/climate.py b/esphome/components/ballu/climate.py index 416fa250cc..e35a1d244d 100644 --- a/esphome/components/ballu/climate.py +++ b/esphome/components/ballu/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@bazuchan"] @@ -9,13 +7,8 @@ CODEOWNERS = ["@bazuchan"] ballu_ns = cg.esphome_ns.namespace("ballu") BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BalluClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 6511270f60..bfdb12278f 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, CONF_HUMIDITY_SENSOR, - CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR, ) @@ -19,9 +18,9 @@ BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Com BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig") CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(BangBangClimate) + .extend( { - cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, @@ -36,15 +35,15 @@ CONFIG_SCHEMA = cv.All( } ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION), ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) diff --git a/esphome/components/bedjet/climate/__init__.py b/esphome/components/bedjet/climate/__init__.py index 7ba3e439b2..e9c5510256 100644 --- a/esphome/components/bedjet/climate/__init__.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -1,11 +1,8 @@ -import logging - import esphome.codegen as cg from esphome.components import ble_client, climate import esphome.config_validation as cv from esphome.const import ( CONF_HEAT_MODE, - CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TEMPERATURE_SOURCE, CONF_TIME_ID, @@ -13,7 +10,6 @@ from esphome.const import ( from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child -_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["bedjet"] @@ -30,9 +26,9 @@ BEDJET_TEMPERATURE_SOURCES = { } CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(BedJetClimate) + .extend( { - cv.GenerateID(): cv.declare_id(BedJetClimate), cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( BEDJET_HEAT_MODES, lower=True ), @@ -63,9 +59,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) await register_bedjet_child(var, config) cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index d8be61397e..32b614e933 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,7 +1,13 @@ +import logging + +from esphome import core import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.cpp_generator import MockObjClass + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["remote_transmitter"] AUTO_LOAD = ["sensor", "remote_base"] @@ -16,30 +22,58 @@ ClimateIR = climate_ir_ns.class_( remote_base.RemoteTransmittable, ) -CLIMATE_IR_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( + +def climate_ir_schema( + class_: MockObjClass, +) -> cv.Schema: + return ( + climate.climate_schema(class_) + .extend( + { + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) + ) + + +def climare_ir_with_receiver_schema( + class_: MockObjClass, +) -> cv.Schema: + return climate_ir_schema(class_).extend( { - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( + remote_base.RemoteReceiverBase + ), } ) - .extend(cv.COMPONENT_SCHEMA) - .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) -) -CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( - { - cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( - remote_base.RemoteReceiverBase - ), - } -) + +# Remove before 2025.11.0 +def deprecated_schema_constant(config): + type: str = "unknown" + if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): + type = str(id.type).split("::", maxsplit=1)[0] + _LOGGER.warning( + "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " + "Please use `climate_ir.climare_ir_with_receiver_schema(...)` instead. " + "If you are seeing this, report an issue to the external_component author and ask them to update it. " + "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " + "Component using this schema: %s", + type, + ) + return config + + +CLIMATE_IR_WITH_RECEIVER_SCHEMA = climare_ir_with_receiver_schema(ClimateIR) +CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) async def register_climate_ir(var, config): await cg.register_component(var, config) - await climate.register_climate(var, config) await remote_base.register_transmittable(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) @@ -48,3 +82,9 @@ async def register_climate_ir(var, config): if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) + + +async def new_climate_ir(config, *args): + var = await climate.new_climate(config, *args) + await register_climate_ir(var, config) + return var diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py index 76d4c00baf..de824bfe5c 100644 --- a/esphome/components/climate_ir_lg/climate.py +++ b/esphome/components/climate_ir_lg/climate.py @@ -1,7 +1,6 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] @@ -14,9 +13,8 @@ CONF_BIT_HIGH = "bit_high" CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ZERO_LOW = "bit_zero_low" -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(LgIrClimate).extend( { - cv.GenerateID(): cv.declare_id(LgIrClimate), cv.Optional( CONF_HEADER_HIGH, default="8000us" ): cv.positive_time_period_microseconds, @@ -37,8 +35,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_header_high(config[CONF_HEADER_HIGH])) cg.add(var.set_header_low(config[CONF_HEADER_LOW])) diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 339e7de906..b280544a5c 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@glmnet"] @@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"] coolix_ns = cg.esphome_ns.namespace("coolix") CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CoolixClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(CoolixClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py index 3946513191..2cd44969c1 100644 --- a/esphome/components/daikin/climate.py +++ b/esphome/components/daikin/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] daikin_ns = cg.esphome_ns.namespace("daikin") DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(DaikinClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/daikin_arc/climate.py b/esphome/components/daikin_arc/climate.py index 967d080c24..8f6b07315d 100644 --- a/esphome/components/daikin_arc/climate.py +++ b/esphome/components/daikin_arc/climate.py @@ -1,18 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(DaikinArcClimate)} -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinArcClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py index aacac408ca..1000784380 100644 --- a/esphome/components/daikin_brc/climate.py +++ b/esphome/components/daikin_brc/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT +from esphome.const import CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir"] @@ -9,15 +9,13 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinBrcClimate).extend( { - cv.GenerateID(): cv.declare_id(DaikinBrcClimate), cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py index 0d3bb76c98..ff878b4ff7 100644 --- a/esphome/components/delonghi/climate.py +++ b/esphome/components/delonghi/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] delonghi_ns = cg.esphome_ns.namespace("delonghi") DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(DelonghiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DelonghiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/emmeti/climate.py b/esphome/components/emmeti/climate.py index b925f4b61e..042f1af64b 100644 --- a/esphome/components/emmeti/climate.py +++ b/esphome/components/emmeti/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID CODEOWNERS = ["@E440QF"] AUTO_LOAD = ["climate_ir"] @@ -9,13 +7,8 @@ AUTO_LOAD = ["climate_ir"] emmeti_ns = cg.esphome_ns.namespace("emmeti") EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EmmetiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(EmmetiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py index 6d2e46512e..0f028d0af3 100644 --- a/esphome/components/fujitsu_general/climate.py +++ b/esphome/components/fujitsu_general/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] @@ -10,13 +8,8 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_( "FujitsuGeneralClimate", climate_ir.ClimateIR ) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(FujitsuGeneralClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py index 389c9fb3c7..947ef9bb97 100644 --- a/esphome/components/gree/climate.py +++ b/esphome/components/gree/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.const import CONF_MODEL CODEOWNERS = ["@orestismers"] @@ -21,16 +21,13 @@ MODELS = { "yag": Model.GREE_YAG, } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(GreeClimate).extend( { - cv.GenerateID(): cv.declare_id(GreeClimate), cv.Required(CONF_MODEL): cv.enum(MODELS), } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) - - await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index f77d624649..0393c263d4 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -30,6 +30,7 @@ from esphome.const import ( CONF_VISUAL, CONF_WIFI, ) +from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) @@ -185,42 +186,46 @@ def validate_visual(config): return config -BASE_CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend( - { - cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( - cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) - ), - cv.Optional( - CONF_SUPPORTED_SWING_MODES, - default=[ - "VERTICAL", - "HORIZONTAL", - "BOTH", - ], - ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), - cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, - cv.Optional(CONF_DISPLAY): cv.boolean, - cv.Optional( - CONF_ANSWER_TIMEOUT, - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusMessageTrigger), - } - ), - } +def _base_config_schema(class_: MockObjClass) -> cv.Schema: + return ( + climate.climate_schema(class_) + .extend( + { + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) + ), + cv.Optional( + CONF_SUPPORTED_SWING_MODES, + default=[ + "VERTICAL", + "HORIZONTAL", + "BOTH", + ], + ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, + cv.Optional(CONF_DISPLAY): cv.boolean, + cv.Optional( + CONF_ANSWER_TIMEOUT, + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + StatusMessageTrigger + ), + } + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) ) - .extend(uart.UART_DEVICE_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) -) + CONFIG_SCHEMA = cv.All( cv.typed_schema( { - PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( + PROTOCOL_SMARTAIR2: _base_config_schema(Smartair2Climate).extend( { - cv.GenerateID(): cv.declare_id(Smartair2Climate), cv.Optional( CONF_ALTERNATIVE_SWING_CONTROL, default=False ): cv.boolean, @@ -232,9 +237,8 @@ CONFIG_SCHEMA = cv.All( ), } ), - PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( + PROTOCOL_HON: _base_config_schema(HonClimate).extend( { - cv.GenerateID(): cv.declare_id(HonClimate), cv.Optional( CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" ): cv.ensure_list( @@ -464,10 +468,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): cg.add(haier_ns.init_haier_protocol_logging()) - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) await uart.register_uart_device(var, config) - await climate.register_climate(var, config) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) if CONF_CONTROL_METHOD in config: diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 612b0d6123..21b0168615 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -2,7 +2,6 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_MAX_TEMPERATURE, CONF_MIN_TEMPERATURE, CONF_PROTOCOL, @@ -98,9 +97,8 @@ VERTICAL_DIRECTIONS = { } CONFIG_SCHEMA = cv.All( - climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + climate_ir.climare_ir_with_receiver_schema(HeatpumpIRClimate).extend( { - cv.GenerateID(): cv.declare_id(HeatpumpIRClimate), cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), cv.Required(CONF_VERTICAL_DEFAULT): cv.enum(VERTICAL_DIRECTIONS), @@ -112,8 +110,8 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def to_code(config): + var = await climate_ir.new_climate_ir(config) if CONF_VISUAL not in config: config[CONF_VISUAL] = {} visual = config[CONF_VISUAL] @@ -121,7 +119,6 @@ def to_code(config): visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE] if CONF_MIN_TEMPERATURE not in visual: visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE] - yield climate_ir.register_climate_ir(var, config) cg.add(var.set_protocol(config[CONF_PROTOCOL])) cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py index 0988d63995..4fa2d54fbb 100644 --- a/esphome/components/hitachi_ac344/climate.py +++ b/esphome/components/hitachi_ac344/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HitachiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/hitachi_ac424/climate.py b/esphome/components/hitachi_ac424/climate.py index 74f3c2fa14..4b20147922 100644 --- a/esphome/components/hitachi_ac424/climate.py +++ b/esphome/components/hitachi_ac424/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HitachiClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 1d3cac66ba..b08a47afa9 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -104,9 +104,9 @@ validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(AirConditioner) + .extend( { - cv.GenerateID(): cv.declare_id(AirConditioner), cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), @@ -259,10 +259,9 @@ async def power_inv_to_code(var, config, args): async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) await uart.register_uart_device(var, config) - await climate.register_climate(var, config) cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 21fa5f4f56..5c9256b5e4 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT +from esphome.const import CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir", "coolix"] CODEOWNERS = ["@dudanov"] @@ -10,15 +10,13 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir") MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MideaIR).extend( { - cv.GenerateID(): cv.declare_id(MideaIR), cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py index 23f8ed21fa..5784d3ee8a 100644 --- a/esphome/components/mitsubishi/climate.py +++ b/esphome/components/mitsubishi/climate.py @@ -1,7 +1,6 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID CODEOWNERS = ["@RubyBailey"] AUTO_LOAD = ["climate_ir"] @@ -44,9 +43,8 @@ VERTICAL_DIRECTIONS = { } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MitsubishiClimate).extend( { - cv.GenerateID(): cv.declare_id(MitsubishiClimate), cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE), cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean, cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean, @@ -61,8 +59,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE])) cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY])) diff --git a/esphome/components/noblex/climate.py b/esphome/components/noblex/climate.py index 7f4e8e6488..d619265d01 100644 --- a/esphome/components/noblex/climate.py +++ b/esphome/components/noblex/climate.py @@ -1,20 +1,13 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] noblex_ns = cg.esphome_ns.namespace("noblex") NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NoblexClimate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(NoblexClimate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index aab7ee5c00..5919d2cac8 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -41,9 +41,8 @@ CONF_KI_MULTIPLIER = "ki_multiplier" CONF_KD_MULTIPLIER = "kd_multiplier" CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(PIDClimate).extend( { - cv.GenerateID(): cv.declare_id(PIDClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, @@ -80,9 +79,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 9cd193f5c7..9864113a52 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@glmnet"] @@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"] tcl112_ns = cg.esphome_ns.namespace("tcl112") Tcl112Climate = tcl112_ns.class_("Tcl112Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(Tcl112Climate), - } -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Tcl112Climate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 638aad7c06..0314d877a3 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -516,9 +516,9 @@ def validate_thermostat(config): CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(ThermostatClimate) + .extend( { - cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), @@ -631,7 +631,8 @@ CONFIG_SCHEMA = cv.All( single=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( CONF_COOL_ACTION, CONF_DRY_ACTION, CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION ), @@ -640,9 +641,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 54582b78a9..40112fc460 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.const import CONF_MODEL AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@kbx81"] @@ -16,15 +16,13 @@ MODELS = { "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ToshibaClimate).extend( { - cv.GenerateID(): cv.declare_id(ToshibaClimate), cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True), } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 371c599ef7..4dbdf07651 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -4,7 +4,6 @@ from esphome.components import climate import esphome.config_validation as cv from esphome.const import ( CONF_FAN_MODE, - CONF_ID, CONF_PRESET, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, @@ -151,9 +150,9 @@ SWING_MODES = cv.Schema( ) CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(TuyaClimate) + .extend( { - cv.GenerateID(): cv.declare_id(TuyaClimate), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean, @@ -186,7 +185,8 @@ CONFIG_SCHEMA = cv.All( "'eco_temperature' has been moved inside of the 'eco' config block under 'preset' as 'temperature'" ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), validate_temperature_multipliers, validate_cooling_values, @@ -194,9 +194,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/uponor_smatrix/climate/__init__.py b/esphome/components/uponor_smatrix/climate/__init__.py index 5aeb521fb1..47495fde9a 100644 --- a/esphome/components/uponor_smatrix/climate/__init__.py +++ b/esphome/components/uponor_smatrix/climate/__init__.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate -import esphome.config_validation as cv -from esphome.const import CONF_ID from .. import ( UPONOR_SMATRIX_DEVICE_SCHEMA, @@ -19,15 +17,12 @@ UponorSmatrixClimate = uponor_smatrix_ns.class_( UponorSmatrixDevice, ) -CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UponorSmatrixClimate), - } -).extend(UPONOR_SMATRIX_DEVICE_SCHEMA) +CONFIG_SCHEMA = climate.climate_schema(UponorSmatrixClimate).extend( + UPONOR_SMATRIX_DEVICE_SCHEMA +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) await register_uponor_smatrix_device(var, config) diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py index 40c6053349..daee9e7fb7 100644 --- a/esphome/components/whirlpool/climate.py +++ b/esphome/components/whirlpool/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.const import CONF_MODEL AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@glmnet"] @@ -15,15 +15,13 @@ MODELS = { "DG11J1-91": Model.MODEL_DG11J1_91, } -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(WhirlpoolClimate).extend( { - cv.GenerateID(): cv.declare_id(WhirlpoolClimate), cv.Optional(CONF_MODEL, default="DG11J1-3A"): cv.enum(MODELS, upper=True), } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py index ae21c64e9b..4a01c014c7 100644 --- a/esphome/components/whynter/climate.py +++ b/esphome/components/whynter/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import climate_ir import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT +from esphome.const import CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir"] @@ -9,15 +9,13 @@ whynter_ns = cg.esphome_ns.namespace("whynter") Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Whynter).extend( { - cv.GenerateID(): cv.declare_id(Whynter), cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py index eb68d3b6e7..d7386eb6a3 100644 --- a/esphome/components/yashima/climate.py +++ b/esphome/components/yashima/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import climate, remote_transmitter, sensor from esphome.components.remote_base import CONF_TRANSMITTER_ID import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT AUTO_LOAD = ["sensor"] @@ -10,9 +10,9 @@ yashima_ns = cg.esphome_ns.namespace("yashima") YashimaClimate = yashima_ns.class_("YashimaClimate", climate.Climate, cg.Component) CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( + climate.climate_schema(YashimaClimate) + .extend( { - cv.GenerateID(): cv.declare_id(YashimaClimate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( remote_transmitter.RemoteTransmitterComponent ), @@ -20,14 +20,14 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), } - ).extend(cv.COMPONENT_SCHEMA) + ) + .extend(cv.COMPONENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await climate.new_climate(config) await cg.register_component(var, config) - await climate.register_climate(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) diff --git a/esphome/components/zhlt01/climate.py b/esphome/components/zhlt01/climate.py index fc01107e1d..d5098ab42c 100644 --- a/esphome/components/zhlt01/climate.py +++ b/esphome/components/zhlt01/climate.py @@ -1,7 +1,5 @@ import esphome.codegen as cg from esphome.components import climate_ir -import esphome.config_validation as cv -from esphome.const import CONF_ID AUTO_LOAD = ["climate_ir"] CODEOWNERS = ["@cfeenstra1024"] @@ -9,11 +7,8 @@ CODEOWNERS = ["@cfeenstra1024"] zhlt01_ns = cg.esphome_ns.namespace("zhlt01") ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(ZHLT01Climate)} -) +CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ZHLT01Climate) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await climate_ir.register_climate_ir(var, config) + await climate_ir.new_climate_ir(config) From 498e3904a9c5aa02315a1490d56ded68b89f8f7b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 19:08:16 +1200 Subject: [PATCH 101/193] Bump esphome-dashboard to 20250514.0 (#8790) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9547cd0ef0..e3b3538d12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 -esphome-dashboard==20250415.0 +esphome-dashboard==20250514.0 aioesphomeapi==30.2.0 zeroconf==0.147.0 puremagic==1.29 From d9839f3a5cb6232c976cdb08b2aa33489a713417 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 21:29:00 +1200 Subject: [PATCH 102/193] Bump version to 2025.5.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0974a673ec..f48de581ea 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0b1" +__version__ = "2025.5.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 5454500024bdd0ad36fc3efca60a86d221377b52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 11:20:22 -0500 Subject: [PATCH 103/193] Bump cairosvg from 2.7.1 to 2.8.0 (#8780) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 328bc33543..106a6ff901 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import esphome-glyphsets==0.2.0 pillow==10.4.0 -cairosvg==2.7.1 +cairosvg==2.8.0 freetype-py==2.5.1 # esp-idf requires this, but doesn't bundle it by default From edb8d187bef60080749689b57c89ea154e7f68a2 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Wed, 14 May 2025 23:15:04 +0200 Subject: [PATCH 104/193] add actions to the MAX7219Component (#6462) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/max7219digit/automation.h | 52 +++++++++++ esphome/components/max7219digit/display.py | 94 +++++++++++++++++++- tests/components/max7219digit/common.yaml | 12 +++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 esphome/components/max7219digit/automation.h diff --git a/esphome/components/max7219digit/automation.h b/esphome/components/max7219digit/automation.h new file mode 100644 index 0000000000..02acebb109 --- /dev/null +++ b/esphome/components/max7219digit/automation.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include "max7219digit.h" + +namespace esphome { +namespace max7219digit { + +template class DisplayInvertAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + bool state = this->state_.value(x...); + this->parent_->invert_on_off(state); + } +}; + +template class DisplayVisibilityAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + bool state = this->state_.value(x...); + this->parent_->turn_on_off(state); + } +}; + +template class DisplayReverseAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + bool state = this->state_.value(x...); + this->parent_->set_reverse(state); + } +}; + +template class DisplayIntensityAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, state) + + void play(Ts... x) override { + uint8_t state = this->state_.value(x...); + this->parent_->set_intensity(state); + } +}; + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index 582d11bf4f..f195078c1a 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -1,7 +1,14 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import display, spi import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS +from esphome.const import ( + CONF_ID, + CONF_INTENSITY, + CONF_LAMBDA, + CONF_NUM_CHIPS, + CONF_STATE, +) CODEOWNERS = ["@rspaargaren"] DEPENDENCIES = ["spi"] @@ -17,6 +24,7 @@ CONF_REVERSE_ENABLE = "reverse_enable" CONF_NUM_CHIP_LINES = "num_chip_lines" CONF_CHIP_LINES_STYLE = "chip_lines_style" + integration_ns = cg.esphome_ns.namespace("max7219digit") ChipLinesStyle = integration_ns.enum("ChipLinesStyle") CHIP_LINES_STYLE = { @@ -99,3 +107,87 @@ async def to_code(config): config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + + +DisplayInvertAction = max7219_ns.class_("DisplayInvertAction", automation.Action) +DisplayVisibilityAction = max7219_ns.class_( + "DisplayVisibilityAction", automation.Action +) +DisplayReverseAction = max7219_ns.class_("DisplayReverseAction", automation.Action) +DisplayIntensityAction = max7219_ns.class_("DisplayIntensityAction", automation.Action) + + +MAX7219_OFF_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(MAX7219Component), + cv.Optional(CONF_STATE, default=False): False, + } +) + +MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(MAX7219Component), + cv.Optional(CONF_STATE, default=True): True, + } +) + + +@automation.register_action( + "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA +) +@automation.register_action( + "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA +) +async def max7129digit_invert_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + cg.add(var.set_state(config[CONF_STATE])) + return var + + +@automation.register_action( + "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA +) +@automation.register_action( + "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA +) +async def max7129digit_visible_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + cg.add(var.set_state(config[CONF_STATE])) + return var + + +@automation.register_action( + "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA +) +@automation.register_action( + "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA +) +async def max7129digit_reverse_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + cg.add(var.set_state(config[CONF_STATE])) + return var + + +MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(MAX7219Component), + cv.Optional(CONF_INTENSITY, default=15): cv.templatable( + cv.int_range(min=0, max=15) + ), + }, + key=CONF_INTENSITY, +) + + +@automation.register_action( + "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA +) +async def max7129digit_intensity_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) + cg.add(var.set_state(template_)) + return var diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index a5a3bd57fb..84edc7eb3d 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -14,3 +14,15 @@ display: id: my_matrix lambda: |- it.printdigit("hello"); + +esphome: + on_boot: + - priority: 100 + then: + - max7129digit.invert_off: + - max7129digit.invert_on: + - max7129digit.turn_on: + - max7129digit.turn_off: + - max7129digit.reverse_on: + - max7129digit.reverse_off: + - max7129digit.intensity: 10 From bb1f24ab43be1ee4e81b98592f46ffa146c170af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 May 2025 21:25:44 -0500 Subject: [PATCH 105/193] Avoid protobuf message construction when tx buffer is full (#8787) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 999 ++++++++-------------- esphome/components/api/api_connection.h | 296 +++++-- 2 files changed, 581 insertions(+), 714 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ee0451f499..c377909951 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -29,7 +29,7 @@ static const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; // helper for allowing only unique entries in the queue -void DeferredMessageQueue::dmq_push_back_with_dedup_(void *source, send_message_t *send_message) { +void DeferredMessageQueue::dmq_push_back_with_dedup_(void *source, send_message_t send_message) { DeferredMessage item(source, send_message); auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), @@ -45,7 +45,7 @@ void DeferredMessageQueue::dmq_push_back_with_dedup_(void *source, send_message_ void DeferredMessageQueue::process_queue() { while (!deferred_queue_.empty()) { DeferredMessage &de = deferred_queue_.front(); - if (de.send_message_(this->api_connection_, de.source_)) { + if ((this->api_connection_->*(de.send_message_))(de.source_)) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); } else { @@ -54,7 +54,7 @@ void DeferredMessageQueue::process_queue() { } } -void DeferredMessageQueue::defer(void *source, send_message_t *send_message) { +void DeferredMessageQueue::defer(void *source, send_message_t send_message) { this->dmq_push_back_with_dedup_(source, send_message); } @@ -153,7 +153,9 @@ void APIConnection::loop() { return; } - this->deferred_message_queue_.process_queue(); + if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) { + this->deferred_message_queue_.process_queue(); + } if (!this->list_entities_iterator_.completed()) this->list_entities_iterator_.advance(); @@ -267,96 +269,65 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_binary_sensor_state(this, binary_sensor, state)) { - this->deferred_message_queue_.defer(binary_sensor, try_send_binary_sensor_state); - } - - return true; + return this->send_state_with_value_(binary_sensor, &APIConnection::try_send_binary_sensor_state_, + &APIConnection::try_send_binary_sensor_state_, state); } void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { - if (!APIConnection::try_send_binary_sensor_info(this, binary_sensor)) { - this->deferred_message_queue_.defer(binary_sensor, try_send_binary_sensor_info); - } + this->send_info_(static_cast(binary_sensor), + reinterpret_cast(&APIConnection::try_send_binary_sensor_info_)); } -bool APIConnection::try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor) { - binary_sensor::BinarySensor *binary_sensor = reinterpret_cast(v_binary_sensor); - return APIConnection::try_send_binary_sensor_state(api, binary_sensor, binary_sensor->state); +bool APIConnection::try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor) { + return this->try_send_binary_sensor_state_(binary_sensor, binary_sensor->state); } -bool APIConnection::try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, - bool state) { - BinarySensorStateResponse resp; - resp.key = binary_sensor->get_object_id_hash(); - resp.state = state; - resp.missing_state = !binary_sensor->has_state(); - return api->send_binary_sensor_state_response(resp); -} -bool APIConnection::try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor) { - binary_sensor::BinarySensor *binary_sensor = reinterpret_cast(v_binary_sensor); - ListEntitiesBinarySensorResponse msg; - msg.object_id = binary_sensor->get_object_id(); +bool APIConnection::try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state) { + BinarySensorStateResponse msg; + msg.state = state; + msg.missing_state = !binary_sensor->has_state(); msg.key = binary_sensor->get_object_id_hash(); - if (binary_sensor->has_own_name()) - msg.name = binary_sensor->get_name(); - msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); + return this->send_binary_sensor_state_response(msg); +} +bool APIConnection::try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor) { + ListEntitiesBinarySensorResponse msg; msg.device_class = binary_sensor->get_device_class(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - msg.disabled_by_default = binary_sensor->is_disabled_by_default(); - msg.icon = binary_sensor->get_icon(); - msg.entity_category = static_cast(binary_sensor->get_entity_category()); - return api->send_list_entities_binary_sensor_response(msg); + msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); + return this->try_send_entity_info_(static_cast(binary_sensor), msg, + &APIConnection::send_list_entities_binary_sensor_response); } #endif #ifdef USE_COVER bool APIConnection::send_cover_state(cover::Cover *cover) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_cover_state(this, cover)) { - this->deferred_message_queue_.defer(cover, try_send_cover_state); - } - - return true; + return this->send_state_(static_cast(cover), + reinterpret_cast(&APIConnection::try_send_cover_state_)); } void APIConnection::send_cover_info(cover::Cover *cover) { - if (!APIConnection::try_send_cover_info(this, cover)) { - this->deferred_message_queue_.defer(cover, try_send_cover_info); - } + this->send_info_(static_cast(cover), + reinterpret_cast(&APIConnection::try_send_cover_info_)); } -bool APIConnection::try_send_cover_state(APIConnection *api, void *v_cover) { - cover::Cover *cover = reinterpret_cast(v_cover); +bool APIConnection::try_send_cover_state_(cover::Cover *cover) { + CoverStateResponse msg; auto traits = cover->get_traits(); - CoverStateResponse resp{}; - resp.key = cover->get_object_id_hash(); - resp.legacy_state = + msg.legacy_state = (cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED; - resp.position = cover->position; + msg.position = cover->position; if (traits.get_supports_tilt()) - resp.tilt = cover->tilt; - resp.current_operation = static_cast(cover->current_operation); - return api->send_cover_state_response(resp); -} -bool APIConnection::try_send_cover_info(APIConnection *api, void *v_cover) { - cover::Cover *cover = reinterpret_cast(v_cover); - auto traits = cover->get_traits(); - ListEntitiesCoverResponse msg; + msg.tilt = cover->tilt; + msg.current_operation = static_cast(cover->current_operation); msg.key = cover->get_object_id_hash(); - msg.object_id = cover->get_object_id(); - if (cover->has_own_name()) - msg.name = cover->get_name(); - msg.unique_id = get_default_unique_id("cover", cover); + return this->send_cover_state_response(msg); +} +bool APIConnection::try_send_cover_info_(cover::Cover *cover) { + ListEntitiesCoverResponse msg; + auto traits = cover->get_traits(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); msg.device_class = cover->get_device_class(); - msg.disabled_by_default = cover->is_disabled_by_default(); - msg.icon = cover->get_icon(); - msg.entity_category = static_cast(cover->get_entity_category()); - return api->send_list_entities_cover_response(msg); + msg.unique_id = get_default_unique_id("cover", cover); + return this->try_send_entity_info_(static_cast(cover), msg, + &APIConnection::send_list_entities_cover_response); } void APIConnection::cover_command(const CoverCommandRequest &msg) { cover::Cover *cover = App.get_cover_by_key(msg.key); @@ -389,56 +360,41 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #ifdef USE_FAN bool APIConnection::send_fan_state(fan::Fan *fan) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_fan_state(this, fan)) { - this->deferred_message_queue_.defer(fan, try_send_fan_state); - } - - return true; + return this->send_state_(static_cast(fan), + reinterpret_cast(&APIConnection::try_send_fan_state_)); } void APIConnection::send_fan_info(fan::Fan *fan) { - if (!APIConnection::try_send_fan_info(this, fan)) { - this->deferred_message_queue_.defer(fan, try_send_fan_info); - } + this->send_info_(static_cast(fan), + reinterpret_cast(&APIConnection::try_send_fan_info_)); } -bool APIConnection::try_send_fan_state(APIConnection *api, void *v_fan) { - fan::Fan *fan = reinterpret_cast(v_fan); +bool APIConnection::try_send_fan_state_(fan::Fan *fan) { + FanStateResponse msg; auto traits = fan->get_traits(); - FanStateResponse resp{}; - resp.key = fan->get_object_id_hash(); - resp.state = fan->state; + msg.state = fan->state; if (traits.supports_oscillation()) - resp.oscillating = fan->oscillating; + msg.oscillating = fan->oscillating; if (traits.supports_speed()) { - resp.speed_level = fan->speed; + msg.speed_level = fan->speed; } if (traits.supports_direction()) - resp.direction = static_cast(fan->direction); + msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes()) - resp.preset_mode = fan->preset_mode; - return api->send_fan_state_response(resp); -} -bool APIConnection::try_send_fan_info(APIConnection *api, void *v_fan) { - fan::Fan *fan = reinterpret_cast(v_fan); - auto traits = fan->get_traits(); - ListEntitiesFanResponse msg; + msg.preset_mode = fan->preset_mode; msg.key = fan->get_object_id_hash(); - msg.object_id = fan->get_object_id(); - if (fan->has_own_name()) - msg.name = fan->get_name(); - msg.unique_id = get_default_unique_id("fan", fan); + return this->send_fan_state_response(msg); +} +bool APIConnection::try_send_fan_info_(fan::Fan *fan) { + ListEntitiesFanResponse msg; + auto traits = fan->get_traits(); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); for (auto const &preset : traits.supported_preset_modes()) msg.supported_preset_modes.push_back(preset); - msg.disabled_by_default = fan->is_disabled_by_default(); - msg.icon = fan->get_icon(); - msg.entity_category = static_cast(fan->get_entity_category()); - return api->send_list_entities_fan_response(msg); + msg.unique_id = get_default_unique_id("fan", fan); + return this->try_send_entity_info_(static_cast(fan), msg, + &APIConnection::send_list_entities_fan_response); } void APIConnection::fan_command(const FanCommandRequest &msg) { fan::Fan *fan = App.get_fan_by_key(msg.key); @@ -464,28 +420,18 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { #ifdef USE_LIGHT bool APIConnection::send_light_state(light::LightState *light) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_light_state(this, light)) { - this->deferred_message_queue_.defer(light, try_send_light_state); - } - - return true; + return this->send_state_(static_cast(light), + reinterpret_cast(&APIConnection::try_send_light_state_)); } void APIConnection::send_light_info(light::LightState *light) { - if (!APIConnection::try_send_light_info(this, light)) { - this->deferred_message_queue_.defer(light, try_send_light_info); - } + this->send_info_(static_cast(light), + reinterpret_cast(&APIConnection::try_send_light_info_)); } -bool APIConnection::try_send_light_state(APIConnection *api, void *v_light) { - light::LightState *light = reinterpret_cast(v_light); +bool APIConnection::try_send_light_state_(light::LightState *light) { + LightStateResponse resp; auto traits = light->get_traits(); auto values = light->remote_values; auto color_mode = values.get_color_mode(); - LightStateResponse resp{}; - - resp.key = light->get_object_id_hash(); resp.state = values.is_on(); resp.color_mode = static_cast(color_mode); resp.brightness = values.get_brightness(); @@ -499,25 +445,14 @@ bool APIConnection::try_send_light_state(APIConnection *api, void *v_light) { resp.warm_white = values.get_warm_white(); if (light->supports_effects()) resp.effect = light->get_effect_name(); - return api->send_light_state_response(resp); + resp.key = light->get_object_id_hash(); + return this->send_light_state_response(resp); } -bool APIConnection::try_send_light_info(APIConnection *api, void *v_light) { - light::LightState *light = reinterpret_cast(v_light); - auto traits = light->get_traits(); +bool APIConnection::try_send_light_info_(light::LightState *light) { ListEntitiesLightResponse msg; - msg.key = light->get_object_id_hash(); - msg.object_id = light->get_object_id(); - if (light->has_own_name()) - msg.name = light->get_name(); - msg.unique_id = get_default_unique_id("light", light); - - msg.disabled_by_default = light->is_disabled_by_default(); - msg.icon = light->get_icon(); - msg.entity_category = static_cast(light->get_entity_category()); - + auto traits = light->get_traits(); for (auto mode : traits.get_supported_color_modes()) msg.supported_color_modes.push_back(static_cast(mode)); - msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); msg.legacy_supports_white_value = @@ -525,17 +460,19 @@ bool APIConnection::try_send_light_info(APIConnection *api, void *v_light) { traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); - if (msg.legacy_supports_color_temperature) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } if (light->supports_effects()) { msg.effects.emplace_back("None"); - for (auto *effect : light->get_effects()) + for (auto *effect : light->get_effects()) { msg.effects.push_back(effect->get_name()); + } } - return api->send_list_entities_light_response(msg); + msg.unique_id = get_default_unique_id("light", light); + return this->try_send_entity_info_(static_cast(light), msg, + &APIConnection::send_list_entities_light_response); } void APIConnection::light_command(const LightCommandRequest &msg) { light::LightState *light = App.get_light_by_key(msg.key); @@ -576,93 +513,65 @@ void APIConnection::light_command(const LightCommandRequest &msg) { #ifdef USE_SENSOR bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_sensor_state(this, sensor, state)) { - this->deferred_message_queue_.defer(sensor, try_send_sensor_state); - } - - return true; + return this->send_state_with_value_(sensor, &APIConnection::try_send_sensor_state_, + &APIConnection::try_send_sensor_state_, state); } void APIConnection::send_sensor_info(sensor::Sensor *sensor) { - if (!APIConnection::try_send_sensor_info(this, sensor)) { - this->deferred_message_queue_.defer(sensor, try_send_sensor_info); - } + this->send_info_(static_cast(sensor), + reinterpret_cast(&APIConnection::try_send_sensor_info_)); } -bool APIConnection::try_send_sensor_state(APIConnection *api, void *v_sensor) { - sensor::Sensor *sensor = reinterpret_cast(v_sensor); - return APIConnection::try_send_sensor_state(api, sensor, sensor->state); +bool APIConnection::try_send_sensor_state_(sensor::Sensor *sensor) { + return this->try_send_sensor_state_(sensor, sensor->state); } -bool APIConnection::try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state) { - SensorStateResponse resp{}; - resp.key = sensor->get_object_id_hash(); +bool APIConnection::try_send_sensor_state_(sensor::Sensor *sensor, float state) { + SensorStateResponse resp; resp.state = state; resp.missing_state = !sensor->has_state(); - return api->send_sensor_state_response(resp); + + resp.key = sensor->get_object_id_hash(); + return this->send_sensor_state_response(resp); } -bool APIConnection::try_send_sensor_info(APIConnection *api, void *v_sensor) { - sensor::Sensor *sensor = reinterpret_cast(v_sensor); +bool APIConnection::try_send_sensor_info_(sensor::Sensor *sensor) { ListEntitiesSensorResponse msg; - msg.key = sensor->get_object_id_hash(); - msg.object_id = sensor->get_object_id(); - if (sensor->has_own_name()) - msg.name = sensor->get_name(); - msg.unique_id = sensor->unique_id(); - if (msg.unique_id.empty()) - msg.unique_id = get_default_unique_id("sensor", sensor); - msg.icon = sensor->get_icon(); msg.unit_of_measurement = sensor->get_unit_of_measurement(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class(); msg.state_class = static_cast(sensor->get_state_class()); - msg.disabled_by_default = sensor->is_disabled_by_default(); - msg.entity_category = static_cast(sensor->get_entity_category()); - return api->send_list_entities_sensor_response(msg); + msg.unique_id = sensor->unique_id(); + if (msg.unique_id.empty()) + msg.unique_id = get_default_unique_id("sensor", sensor); + return this->try_send_entity_info_(static_cast(sensor), msg, + &APIConnection::send_list_entities_sensor_response); } #endif #ifdef USE_SWITCH bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_switch_state(this, a_switch, state)) { - this->deferred_message_queue_.defer(a_switch, try_send_switch_state); - } - - return true; + return this->send_state_with_value_(a_switch, &APIConnection::try_send_switch_state_, + &APIConnection::try_send_switch_state_, state); } void APIConnection::send_switch_info(switch_::Switch *a_switch) { - if (!APIConnection::try_send_switch_info(this, a_switch)) { - this->deferred_message_queue_.defer(a_switch, try_send_switch_info); - } + this->send_info_(static_cast(a_switch), + reinterpret_cast(&APIConnection::try_send_switch_info_)); } -bool APIConnection::try_send_switch_state(APIConnection *api, void *v_a_switch) { - switch_::Switch *a_switch = reinterpret_cast(v_a_switch); - return APIConnection::try_send_switch_state(api, a_switch, a_switch->state); +bool APIConnection::try_send_switch_state_(switch_::Switch *a_switch) { + return this->try_send_switch_state_(a_switch, a_switch->state); } -bool APIConnection::try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state) { - SwitchStateResponse resp{}; - resp.key = a_switch->get_object_id_hash(); +bool APIConnection::try_send_switch_state_(switch_::Switch *a_switch, bool state) { + SwitchStateResponse resp; resp.state = state; - return api->send_switch_state_response(resp); + + resp.key = a_switch->get_object_id_hash(); + return this->send_switch_state_response(resp); } -bool APIConnection::try_send_switch_info(APIConnection *api, void *v_a_switch) { - switch_::Switch *a_switch = reinterpret_cast(v_a_switch); +bool APIConnection::try_send_switch_info_(switch_::Switch *a_switch) { ListEntitiesSwitchResponse msg; - msg.key = a_switch->get_object_id_hash(); - msg.object_id = a_switch->get_object_id(); - if (a_switch->has_own_name()) - msg.name = a_switch->get_name(); - msg.unique_id = get_default_unique_id("switch", a_switch); - msg.icon = a_switch->get_icon(); msg.assumed_state = a_switch->assumed_state(); - msg.disabled_by_default = a_switch->is_disabled_by_default(); - msg.entity_category = static_cast(a_switch->get_entity_category()); msg.device_class = a_switch->get_device_class(); - return api->send_list_entities_switch_response(msg); + msg.unique_id = get_default_unique_id("switch", a_switch); + return this->try_send_entity_info_(static_cast(a_switch), msg, + &APIConnection::send_list_entities_switch_response); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { switch_::Switch *a_switch = App.get_switch_by_key(msg.key); @@ -679,70 +588,48 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) { #ifdef USE_TEXT_SENSOR bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_text_sensor_state(this, text_sensor, std::move(state))) { - this->deferred_message_queue_.defer(text_sensor, try_send_text_sensor_state); - } - - return true; + return this->send_state_with_value_(text_sensor, &APIConnection::try_send_text_sensor_state_, + &APIConnection::try_send_text_sensor_state_, std::move(state)); } void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { - if (!APIConnection::try_send_text_sensor_info(this, text_sensor)) { - this->deferred_message_queue_.defer(text_sensor, try_send_text_sensor_info); - } + this->send_info_(static_cast(text_sensor), + reinterpret_cast(&APIConnection::try_send_text_sensor_info_)); } -bool APIConnection::try_send_text_sensor_state(APIConnection *api, void *v_text_sensor) { - text_sensor::TextSensor *text_sensor = reinterpret_cast(v_text_sensor); - return APIConnection::try_send_text_sensor_state(api, text_sensor, text_sensor->state); +bool APIConnection::try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor) { + return this->try_send_text_sensor_state_(text_sensor, text_sensor->state); } -bool APIConnection::try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, - std::string state) { - TextSensorStateResponse resp{}; - resp.key = text_sensor->get_object_id_hash(); +bool APIConnection::try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state) { + TextSensorStateResponse resp; resp.state = std::move(state); resp.missing_state = !text_sensor->has_state(); - return api->send_text_sensor_state_response(resp); + + resp.key = text_sensor->get_object_id_hash(); + return this->send_text_sensor_state_response(resp); } -bool APIConnection::try_send_text_sensor_info(APIConnection *api, void *v_text_sensor) { - text_sensor::TextSensor *text_sensor = reinterpret_cast(v_text_sensor); +bool APIConnection::try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor) { ListEntitiesTextSensorResponse msg; - msg.key = text_sensor->get_object_id_hash(); - msg.object_id = text_sensor->get_object_id(); - msg.name = text_sensor->get_name(); + msg.device_class = text_sensor->get_device_class(); msg.unique_id = text_sensor->unique_id(); if (msg.unique_id.empty()) msg.unique_id = get_default_unique_id("text_sensor", text_sensor); - msg.icon = text_sensor->get_icon(); - msg.disabled_by_default = text_sensor->is_disabled_by_default(); - msg.entity_category = static_cast(text_sensor->get_entity_category()); - msg.device_class = text_sensor->get_device_class(); - return api->send_list_entities_text_sensor_response(msg); + return this->try_send_entity_info_(static_cast(text_sensor), msg, + &APIConnection::send_list_entities_text_sensor_response); } #endif #ifdef USE_CLIMATE bool APIConnection::send_climate_state(climate::Climate *climate) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_climate_state(this, climate)) { - this->deferred_message_queue_.defer(climate, try_send_climate_state); - } - - return true; + return this->send_state_(static_cast(climate), + reinterpret_cast(&APIConnection::try_send_climate_state_)); } void APIConnection::send_climate_info(climate::Climate *climate) { - if (!APIConnection::try_send_climate_info(this, climate)) { - this->deferred_message_queue_.defer(climate, try_send_climate_info); - } + this->send_info_(static_cast(climate), + reinterpret_cast(&APIConnection::try_send_climate_info_)); } -bool APIConnection::try_send_climate_state(APIConnection *api, void *v_climate) { - climate::Climate *climate = reinterpret_cast(v_climate); - auto traits = climate->get_traits(); - ClimateStateResponse resp{}; +bool APIConnection::try_send_climate_state_(climate::Climate *climate) { + ClimateStateResponse resp; resp.key = climate->get_object_id_hash(); + auto traits = climate->get_traits(); resp.mode = static_cast(climate->mode); resp.action = static_cast(climate->action); if (traits.get_supports_current_temperature()) @@ -768,40 +655,25 @@ bool APIConnection::try_send_climate_state(APIConnection *api, void *v_climate) resp.current_humidity = climate->current_humidity; if (traits.get_supports_target_humidity()) resp.target_humidity = climate->target_humidity; - return api->send_climate_state_response(resp); + return this->send_climate_state_response(resp); } -bool APIConnection::try_send_climate_info(APIConnection *api, void *v_climate) { - climate::Climate *climate = reinterpret_cast(v_climate); - auto traits = climate->get_traits(); +bool APIConnection::try_send_climate_info_(climate::Climate *climate) { ListEntitiesClimateResponse msg; - msg.key = climate->get_object_id_hash(); - msg.object_id = climate->get_object_id(); - if (climate->has_own_name()) - msg.name = climate->get_name(); - msg.unique_id = get_default_unique_id("climate", climate); - - msg.disabled_by_default = climate->is_disabled_by_default(); - msg.icon = climate->get_icon(); - msg.entity_category = static_cast(climate->get_entity_category()); - + auto traits = climate->get_traits(); msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); msg.supports_target_humidity = traits.get_supports_target_humidity(); - for (auto mode : traits.get_supported_modes()) msg.supported_modes.push_back(static_cast(mode)); - msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); - for (auto fan_mode : traits.get_supported_fan_modes()) msg.supported_fan_modes.push_back(static_cast(fan_mode)); for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) @@ -812,7 +684,9 @@ bool APIConnection::try_send_climate_info(APIConnection *api, void *v_climate) { msg.supported_custom_presets.push_back(custom_preset); for (auto swing_mode : traits.get_supported_swing_modes()) msg.supported_swing_modes.push_back(static_cast(swing_mode)); - return api->send_list_entities_climate_response(msg); + msg.unique_id = get_default_unique_id("climate", climate); + return this->try_send_entity_info_(static_cast(climate), msg, + &APIConnection::send_list_entities_climate_response); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { climate::Climate *climate = App.get_climate_by_key(msg.key); @@ -846,51 +720,35 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { #ifdef USE_NUMBER bool APIConnection::send_number_state(number::Number *number, float state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_number_state(this, number, state)) { - this->deferred_message_queue_.defer(number, try_send_number_state); - } - - return true; + return this->send_state_with_value_(number, &APIConnection::try_send_number_state_, + &APIConnection::try_send_number_state_, state); } void APIConnection::send_number_info(number::Number *number) { - if (!APIConnection::try_send_number_info(this, number)) { - this->deferred_message_queue_.defer(number, try_send_number_info); - } + this->send_info_(static_cast(number), + reinterpret_cast(&APIConnection::try_send_number_info_)); } -bool APIConnection::try_send_number_state(APIConnection *api, void *v_number) { - number::Number *number = reinterpret_cast(v_number); - return APIConnection::try_send_number_state(api, number, number->state); +bool APIConnection::try_send_number_state_(number::Number *number) { + return this->try_send_number_state_(number, number->state); } -bool APIConnection::try_send_number_state(APIConnection *api, number::Number *number, float state) { - NumberStateResponse resp{}; - resp.key = number->get_object_id_hash(); +bool APIConnection::try_send_number_state_(number::Number *number, float state) { + NumberStateResponse resp; resp.state = state; resp.missing_state = !number->has_state(); - return api->send_number_state_response(resp); + + resp.key = number->get_object_id_hash(); + return this->send_number_state_response(resp); } -bool APIConnection::try_send_number_info(APIConnection *api, void *v_number) { - number::Number *number = reinterpret_cast(v_number); +bool APIConnection::try_send_number_info_(number::Number *number) { ListEntitiesNumberResponse msg; - msg.key = number->get_object_id_hash(); - msg.object_id = number->get_object_id(); - if (number->has_own_name()) - msg.name = number->get_name(); - msg.unique_id = get_default_unique_id("number", number); - msg.icon = number->get_icon(); - msg.disabled_by_default = number->is_disabled_by_default(); - msg.entity_category = static_cast(number->get_entity_category()); msg.unit_of_measurement = number->traits.get_unit_of_measurement(); msg.mode = static_cast(number->traits.get_mode()); msg.device_class = number->traits.get_device_class(); - msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - - return api->send_list_entities_number_response(msg); + msg.unique_id = get_default_unique_id("number", number); + return this->try_send_entity_info_(static_cast(number), msg, + &APIConnection::send_list_entities_number_response); } void APIConnection::number_command(const NumberCommandRequest &msg) { number::Number *number = App.get_number_by_key(msg.key); @@ -905,43 +763,28 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { #ifdef USE_DATETIME_DATE bool APIConnection::send_date_state(datetime::DateEntity *date) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_date_state(this, date)) { - this->deferred_message_queue_.defer(date, try_send_date_state); - } - - return true; + return this->send_state_(static_cast(date), + reinterpret_cast(&APIConnection::try_send_date_state_)); } void APIConnection::send_date_info(datetime::DateEntity *date) { - if (!APIConnection::try_send_date_info(this, date)) { - this->deferred_message_queue_.defer(date, try_send_date_info); - } + this->send_info_(static_cast(date), + reinterpret_cast(&APIConnection::try_send_date_info_)); } -bool APIConnection::try_send_date_state(APIConnection *api, void *v_date) { - datetime::DateEntity *date = reinterpret_cast(v_date); - DateStateResponse resp{}; - resp.key = date->get_object_id_hash(); +bool APIConnection::try_send_date_state_(datetime::DateEntity *date) { + DateStateResponse resp; resp.missing_state = !date->has_state(); resp.year = date->year; resp.month = date->month; resp.day = date->day; - return api->send_date_state_response(resp); -} -bool APIConnection::try_send_date_info(APIConnection *api, void *v_date) { - datetime::DateEntity *date = reinterpret_cast(v_date); - ListEntitiesDateResponse msg; - msg.key = date->get_object_id_hash(); - msg.object_id = date->get_object_id(); - if (date->has_own_name()) - msg.name = date->get_name(); - msg.unique_id = get_default_unique_id("date", date); - msg.icon = date->get_icon(); - msg.disabled_by_default = date->is_disabled_by_default(); - msg.entity_category = static_cast(date->get_entity_category()); - return api->send_list_entities_date_response(msg); + resp.key = date->get_object_id_hash(); + return this->send_date_state_response(resp); +} +bool APIConnection::try_send_date_info_(datetime::DateEntity *date) { + ListEntitiesDateResponse msg; + msg.unique_id = get_default_unique_id("date", date); + return this->try_send_entity_info_(static_cast(date), msg, + &APIConnection::send_list_entities_date_response); } void APIConnection::date_command(const DateCommandRequest &msg) { datetime::DateEntity *date = App.get_date_by_key(msg.key); @@ -956,43 +799,28 @@ void APIConnection::date_command(const DateCommandRequest &msg) { #ifdef USE_DATETIME_TIME bool APIConnection::send_time_state(datetime::TimeEntity *time) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_time_state(this, time)) { - this->deferred_message_queue_.defer(time, try_send_time_state); - } - - return true; + return this->send_state_(static_cast(time), + reinterpret_cast(&APIConnection::try_send_time_state_)); } void APIConnection::send_time_info(datetime::TimeEntity *time) { - if (!APIConnection::try_send_time_info(this, time)) { - this->deferred_message_queue_.defer(time, try_send_time_info); - } + this->send_info_(static_cast(time), + reinterpret_cast(&APIConnection::try_send_time_info_)); } -bool APIConnection::try_send_time_state(APIConnection *api, void *v_time) { - datetime::TimeEntity *time = reinterpret_cast(v_time); - TimeStateResponse resp{}; - resp.key = time->get_object_id_hash(); +bool APIConnection::try_send_time_state_(datetime::TimeEntity *time) { + TimeStateResponse resp; resp.missing_state = !time->has_state(); resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return api->send_time_state_response(resp); -} -bool APIConnection::try_send_time_info(APIConnection *api, void *v_time) { - datetime::TimeEntity *time = reinterpret_cast(v_time); - ListEntitiesTimeResponse msg; - msg.key = time->get_object_id_hash(); - msg.object_id = time->get_object_id(); - if (time->has_own_name()) - msg.name = time->get_name(); - msg.unique_id = get_default_unique_id("time", time); - msg.icon = time->get_icon(); - msg.disabled_by_default = time->is_disabled_by_default(); - msg.entity_category = static_cast(time->get_entity_category()); - return api->send_list_entities_time_response(msg); + resp.key = time->get_object_id_hash(); + return this->send_time_state_response(resp); +} +bool APIConnection::try_send_time_info_(datetime::TimeEntity *time) { + ListEntitiesTimeResponse msg; + msg.unique_id = get_default_unique_id("time", time); + return this->try_send_entity_info_(static_cast(time), msg, + &APIConnection::send_list_entities_time_response); } void APIConnection::time_command(const TimeCommandRequest &msg) { datetime::TimeEntity *time = App.get_time_by_key(msg.key); @@ -1007,44 +835,29 @@ void APIConnection::time_command(const TimeCommandRequest &msg) { #ifdef USE_DATETIME_DATETIME bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_datetime_state(this, datetime)) { - this->deferred_message_queue_.defer(datetime, try_send_datetime_state); - } - - return true; + return this->send_state_(static_cast(datetime), + reinterpret_cast(&APIConnection::try_send_datetime_state_)); } void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { - if (!APIConnection::try_send_datetime_info(this, datetime)) { - this->deferred_message_queue_.defer(datetime, try_send_datetime_info); - } + this->send_info_(static_cast(datetime), + reinterpret_cast(&APIConnection::try_send_datetime_info_)); } -bool APIConnection::try_send_datetime_state(APIConnection *api, void *v_datetime) { - datetime::DateTimeEntity *datetime = reinterpret_cast(v_datetime); - DateTimeStateResponse resp{}; - resp.key = datetime->get_object_id_hash(); +bool APIConnection::try_send_datetime_state_(datetime::DateTimeEntity *datetime) { + DateTimeStateResponse resp; resp.missing_state = !datetime->has_state(); if (datetime->has_state()) { ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return api->send_date_time_state_response(resp); -} -bool APIConnection::try_send_datetime_info(APIConnection *api, void *v_datetime) { - datetime::DateTimeEntity *datetime = reinterpret_cast(v_datetime); - ListEntitiesDateTimeResponse msg; - msg.key = datetime->get_object_id_hash(); - msg.object_id = datetime->get_object_id(); - if (datetime->has_own_name()) - msg.name = datetime->get_name(); - msg.unique_id = get_default_unique_id("datetime", datetime); - msg.icon = datetime->get_icon(); - msg.disabled_by_default = datetime->is_disabled_by_default(); - msg.entity_category = static_cast(datetime->get_entity_category()); - return api->send_list_entities_date_time_response(msg); + resp.key = datetime->get_object_id_hash(); + return this->send_date_time_state_response(resp); +} +bool APIConnection::try_send_datetime_info_(datetime::DateTimeEntity *datetime) { + ListEntitiesDateTimeResponse msg; + msg.unique_id = get_default_unique_id("datetime", datetime); + return this->try_send_entity_info_(static_cast(datetime), msg, + &APIConnection::send_list_entities_date_time_response); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); @@ -1059,47 +872,31 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { #ifdef USE_TEXT bool APIConnection::send_text_state(text::Text *text, std::string state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_text_state(this, text, std::move(state))) { - this->deferred_message_queue_.defer(text, try_send_text_state); - } - - return true; + return this->send_state_with_value_(text, &APIConnection::try_send_text_state_, &APIConnection::try_send_text_state_, + std::move(state)); } void APIConnection::send_text_info(text::Text *text) { - if (!APIConnection::try_send_text_info(this, text)) { - this->deferred_message_queue_.defer(text, try_send_text_info); - } + this->send_info_(static_cast(text), + reinterpret_cast(&APIConnection::try_send_text_info_)); } -bool APIConnection::try_send_text_state(APIConnection *api, void *v_text) { - text::Text *text = reinterpret_cast(v_text); - return APIConnection::try_send_text_state(api, text, text->state); -} -bool APIConnection::try_send_text_state(APIConnection *api, text::Text *text, std::string state) { - TextStateResponse resp{}; - resp.key = text->get_object_id_hash(); +bool APIConnection::try_send_text_state_(text::Text *text) { return this->try_send_text_state_(text, text->state); } +bool APIConnection::try_send_text_state_(text::Text *text, std::string state) { + TextStateResponse resp; resp.state = std::move(state); resp.missing_state = !text->has_state(); - return api->send_text_state_response(resp); -} -bool APIConnection::try_send_text_info(APIConnection *api, void *v_text) { - text::Text *text = reinterpret_cast(v_text); - ListEntitiesTextResponse msg; - msg.key = text->get_object_id_hash(); - msg.object_id = text->get_object_id(); - msg.name = text->get_name(); - msg.icon = text->get_icon(); - msg.disabled_by_default = text->is_disabled_by_default(); - msg.entity_category = static_cast(text->get_entity_category()); - msg.mode = static_cast(text->traits.get_mode()); + resp.key = text->get_object_id_hash(); + return this->send_text_state_response(resp); +} +bool APIConnection::try_send_text_info_(text::Text *text) { + ListEntitiesTextResponse msg; + msg.mode = static_cast(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern(); - - return api->send_list_entities_text_response(msg); + msg.unique_id = get_default_unique_id("text", text); + return this->try_send_entity_info_(static_cast(text), msg, + &APIConnection::send_list_entities_text_response); } void APIConnection::text_command(const TextCommandRequest &msg) { text::Text *text = App.get_text_by_key(msg.key); @@ -1114,47 +911,31 @@ void APIConnection::text_command(const TextCommandRequest &msg) { #ifdef USE_SELECT bool APIConnection::send_select_state(select::Select *select, std::string state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_select_state(this, select, std::move(state))) { - this->deferred_message_queue_.defer(select, try_send_select_state); - } - - return true; + return this->send_state_with_value_(select, &APIConnection::try_send_select_state_, + &APIConnection::try_send_select_state_, std::move(state)); } void APIConnection::send_select_info(select::Select *select) { - if (!APIConnection::try_send_select_info(this, select)) { - this->deferred_message_queue_.defer(select, try_send_select_info); - } + this->send_info_(static_cast(select), + reinterpret_cast(&APIConnection::try_send_select_info_)); } -bool APIConnection::try_send_select_state(APIConnection *api, void *v_select) { - select::Select *select = reinterpret_cast(v_select); - return APIConnection::try_send_select_state(api, select, select->state); +bool APIConnection::try_send_select_state_(select::Select *select) { + return this->try_send_select_state_(select, select->state); } -bool APIConnection::try_send_select_state(APIConnection *api, select::Select *select, std::string state) { - SelectStateResponse resp{}; - resp.key = select->get_object_id_hash(); +bool APIConnection::try_send_select_state_(select::Select *select, std::string state) { + SelectStateResponse resp; resp.state = std::move(state); resp.missing_state = !select->has_state(); - return api->send_select_state_response(resp); -} -bool APIConnection::try_send_select_info(APIConnection *api, void *v_select) { - select::Select *select = reinterpret_cast(v_select); - ListEntitiesSelectResponse msg; - msg.key = select->get_object_id_hash(); - msg.object_id = select->get_object_id(); - if (select->has_own_name()) - msg.name = select->get_name(); - msg.unique_id = get_default_unique_id("select", select); - msg.icon = select->get_icon(); - msg.disabled_by_default = select->is_disabled_by_default(); - msg.entity_category = static_cast(select->get_entity_category()); + resp.key = select->get_object_id_hash(); + return this->send_select_state_response(resp); +} +bool APIConnection::try_send_select_info_(select::Select *select) { + ListEntitiesSelectResponse msg; for (const auto &option : select->traits.get_options()) msg.options.push_back(option); - - return api->send_list_entities_select_response(msg); + msg.unique_id = get_default_unique_id("select", select); + return this->try_send_entity_info_(static_cast(select), msg, + &APIConnection::send_list_entities_select_response); } void APIConnection::select_command(const SelectCommandRequest &msg) { select::Select *select = App.get_select_by_key(msg.key); @@ -1168,26 +949,18 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -void APIConnection::send_button_info(button::Button *button) { - if (!APIConnection::try_send_button_info(this, button)) { - this->deferred_message_queue_.defer(button, try_send_button_info); - } +void esphome::api::APIConnection::send_button_info(button::Button *button) { + this->send_info_(static_cast(button), + reinterpret_cast(&APIConnection::try_send_button_info_)); } -bool APIConnection::try_send_button_info(APIConnection *api, void *v_button) { - button::Button *button = reinterpret_cast(v_button); +bool esphome::api::APIConnection::try_send_button_info_(button::Button *button) { ListEntitiesButtonResponse msg; - msg.key = button->get_object_id_hash(); - msg.object_id = button->get_object_id(); - if (button->has_own_name()) - msg.name = button->get_name(); - msg.unique_id = get_default_unique_id("button", button); - msg.icon = button->get_icon(); - msg.disabled_by_default = button->is_disabled_by_default(); - msg.entity_category = static_cast(button->get_entity_category()); msg.device_class = button->get_device_class(); - return api->send_list_entities_button_response(msg); + msg.unique_id = get_default_unique_id("button", button); + return this->try_send_entity_info_(static_cast(button), msg, + &APIConnection::send_list_entities_button_response); } -void APIConnection::button_command(const ButtonCommandRequest &msg) { +void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { button::Button *button = App.get_button_by_key(msg.key); if (button == nullptr) return; @@ -1198,45 +971,31 @@ void APIConnection::button_command(const ButtonCommandRequest &msg) { #ifdef USE_LOCK bool APIConnection::send_lock_state(lock::Lock *a_lock, lock::LockState state) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_lock_state(this, a_lock, state)) { - this->deferred_message_queue_.defer(a_lock, try_send_lock_state); - } - - return true; + return this->send_state_with_value_(a_lock, &APIConnection::try_send_lock_state_, + &APIConnection::try_send_lock_state_, state); } void APIConnection::send_lock_info(lock::Lock *a_lock) { - if (!APIConnection::try_send_lock_info(this, a_lock)) { - this->deferred_message_queue_.defer(a_lock, try_send_lock_info); - } + this->send_info_(static_cast(a_lock), + reinterpret_cast(&APIConnection::try_send_lock_info_)); } -bool APIConnection::try_send_lock_state(APIConnection *api, void *v_a_lock) { - lock::Lock *a_lock = reinterpret_cast(v_a_lock); - return APIConnection::try_send_lock_state(api, a_lock, a_lock->state); +bool APIConnection::try_send_lock_state_(lock::Lock *a_lock) { + return this->try_send_lock_state_(a_lock, a_lock->state); } -bool APIConnection::try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state) { - LockStateResponse resp{}; - resp.key = a_lock->get_object_id_hash(); +bool APIConnection::try_send_lock_state_(lock::Lock *a_lock, lock::LockState state) { + LockStateResponse resp; resp.state = static_cast(state); - return api->send_lock_state_response(resp); + + resp.key = a_lock->get_object_id_hash(); + return this->send_lock_state_response(resp); } -bool APIConnection::try_send_lock_info(APIConnection *api, void *v_a_lock) { - lock::Lock *a_lock = reinterpret_cast(v_a_lock); +bool APIConnection::try_send_lock_info_(lock::Lock *a_lock) { ListEntitiesLockResponse msg; - msg.key = a_lock->get_object_id_hash(); - msg.object_id = a_lock->get_object_id(); - if (a_lock->has_own_name()) - msg.name = a_lock->get_name(); - msg.unique_id = get_default_unique_id("lock", a_lock); - msg.icon = a_lock->get_icon(); msg.assumed_state = a_lock->traits.get_assumed_state(); - msg.disabled_by_default = a_lock->is_disabled_by_default(); - msg.entity_category = static_cast(a_lock->get_entity_category()); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return api->send_list_entities_lock_response(msg); + msg.unique_id = get_default_unique_id("lock", a_lock); + return this->try_send_entity_info_(static_cast(a_lock), msg, + &APIConnection::send_list_entities_lock_response); } void APIConnection::lock_command(const LockCommandRequest &msg) { lock::Lock *a_lock = App.get_lock_by_key(msg.key); @@ -1259,45 +1018,31 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { #ifdef USE_VALVE bool APIConnection::send_valve_state(valve::Valve *valve) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_valve_state(this, valve)) { - this->deferred_message_queue_.defer(valve, try_send_valve_state); - } - - return true; + return this->send_state_(static_cast(valve), + reinterpret_cast(&APIConnection::try_send_valve_state_)); } void APIConnection::send_valve_info(valve::Valve *valve) { - if (!APIConnection::try_send_valve_info(this, valve)) { - this->deferred_message_queue_.defer(valve, try_send_valve_info); - } + this->send_info_(static_cast(valve), + reinterpret_cast(&APIConnection::try_send_valve_info_)); } -bool APIConnection::try_send_valve_state(APIConnection *api, void *v_valve) { - valve::Valve *valve = reinterpret_cast(v_valve); - ValveStateResponse resp{}; - resp.key = valve->get_object_id_hash(); +bool APIConnection::try_send_valve_state_(valve::Valve *valve) { + ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - return api->send_valve_state_response(resp); + + resp.key = valve->get_object_id_hash(); + return this->send_valve_state_response(resp); } -bool APIConnection::try_send_valve_info(APIConnection *api, void *v_valve) { - valve::Valve *valve = reinterpret_cast(v_valve); - auto traits = valve->get_traits(); +bool APIConnection::try_send_valve_info_(valve::Valve *valve) { ListEntitiesValveResponse msg; - msg.key = valve->get_object_id_hash(); - msg.object_id = valve->get_object_id(); - if (valve->has_own_name()) - msg.name = valve->get_name(); - msg.unique_id = get_default_unique_id("valve", valve); - msg.icon = valve->get_icon(); - msg.disabled_by_default = valve->is_disabled_by_default(); - msg.entity_category = static_cast(valve->get_entity_category()); + auto traits = valve->get_traits(); msg.device_class = valve->get_device_class(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return api->send_list_entities_valve_response(msg); + msg.unique_id = get_default_unique_id("valve", valve); + return this->try_send_entity_info_(static_cast(valve), msg, + &APIConnection::send_list_entities_valve_response); } void APIConnection::valve_command(const ValveCommandRequest &msg) { valve::Valve *valve = App.get_valve_by_key(msg.key); @@ -1315,48 +1060,29 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) { #ifdef USE_MEDIA_PLAYER bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_media_player_state(this, media_player)) { - this->deferred_message_queue_.defer(media_player, try_send_media_player_state); - } - - return true; + return this->send_state_(static_cast(media_player), + reinterpret_cast(&APIConnection::try_send_media_player_state_)); } void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { - if (!APIConnection::try_send_media_player_info(this, media_player)) { - this->deferred_message_queue_.defer(media_player, try_send_media_player_info); - } + this->send_info_(static_cast(media_player), + reinterpret_cast(&APIConnection::try_send_media_player_info_)); } -bool APIConnection::try_send_media_player_state(APIConnection *api, void *v_media_player) { - media_player::MediaPlayer *media_player = reinterpret_cast(v_media_player); - MediaPlayerStateResponse resp{}; - resp.key = media_player->get_object_id_hash(); - +bool APIConnection::try_send_media_player_state_(media_player::MediaPlayer *media_player) { + MediaPlayerStateResponse resp; media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING ? media_player::MEDIA_PLAYER_STATE_PLAYING : media_player->state; resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return api->send_media_player_state_response(resp); -} -bool APIConnection::try_send_media_player_info(APIConnection *api, void *v_media_player) { - media_player::MediaPlayer *media_player = reinterpret_cast(v_media_player); - ListEntitiesMediaPlayerResponse msg; - msg.key = media_player->get_object_id_hash(); - msg.object_id = media_player->get_object_id(); - if (media_player->has_own_name()) - msg.name = media_player->get_name(); - msg.unique_id = get_default_unique_id("media_player", media_player); - msg.icon = media_player->get_icon(); - msg.disabled_by_default = media_player->is_disabled_by_default(); - msg.entity_category = static_cast(media_player->get_entity_category()); + resp.key = media_player->get_object_id_hash(); + return this->send_media_player_state_response(resp); +} +bool APIConnection::try_send_media_player_info_(media_player::MediaPlayer *media_player) { + ListEntitiesMediaPlayerResponse msg; auto traits = media_player->get_traits(); msg.supports_pause = traits.get_supports_pause(); - for (auto &supported_format : traits.get_supported_formats()) { MediaPlayerSupportedFormat media_format; media_format.format = supported_format.format; @@ -1366,8 +1092,9 @@ bool APIConnection::try_send_media_player_info(APIConnection *api, void *v_media media_format.sample_bytes = supported_format.sample_bytes; msg.supported_formats.push_back(media_format); } - - return api->send_list_entities_media_player_response(msg); + msg.unique_id = get_default_unique_id("media_player", media_player); + return this->try_send_entity_info_(static_cast(media_player), msg, + &APIConnection::send_list_entities_media_player_response); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); @@ -1402,22 +1129,14 @@ void APIConnection::set_camera_state(std::shared_ptr this->image_reader_.set_image(std::move(image)); } void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { - if (!APIConnection::try_send_camera_info(this, camera)) { - this->deferred_message_queue_.defer(camera, try_send_camera_info); - } + this->send_info_(static_cast(camera), + reinterpret_cast(&APIConnection::try_send_camera_info_)); } -bool APIConnection::try_send_camera_info(APIConnection *api, void *v_camera) { - esp32_camera::ESP32Camera *camera = reinterpret_cast(v_camera); +bool APIConnection::try_send_camera_info_(esp32_camera::ESP32Camera *camera) { ListEntitiesCameraResponse msg; - msg.key = camera->get_object_id_hash(); - msg.object_id = camera->get_object_id(); - if (camera->has_own_name()) - msg.name = camera->get_name(); msg.unique_id = get_default_unique_id("camera", camera); - msg.disabled_by_default = camera->is_disabled_by_default(); - msg.icon = camera->get_icon(); - msg.entity_category = static_cast(camera->get_entity_category()); - return api->send_list_entities_camera_response(msg); + return this->try_send_entity_info_(static_cast(camera), msg, + &APIConnection::send_list_entities_camera_response); } void APIConnection::camera_image(const CameraImageRequest &msg) { if (esp32_camera::global_esp32_camera == nullptr) @@ -1606,43 +1325,28 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon #ifdef USE_ALARM_CONTROL_PANEL bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_alarm_control_panel_state(this, a_alarm_control_panel)) { - this->deferred_message_queue_.defer(a_alarm_control_panel, try_send_alarm_control_panel_state); - } - - return true; + return this->send_state_(static_cast(a_alarm_control_panel), + reinterpret_cast(&APIConnection::try_send_alarm_control_panel_state_)); } void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - if (!APIConnection::try_send_alarm_control_panel_info(this, a_alarm_control_panel)) { - this->deferred_message_queue_.defer(a_alarm_control_panel, try_send_alarm_control_panel_info); - } + this->send_info_(static_cast(a_alarm_control_panel), + reinterpret_cast(&APIConnection::try_send_alarm_control_panel_info_)); } -bool APIConnection::try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel) { - alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = - reinterpret_cast(v_a_alarm_control_panel); - AlarmControlPanelStateResponse resp{}; - resp.key = a_alarm_control_panel->get_object_id_hash(); +bool APIConnection::try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - return api->send_alarm_control_panel_state_response(resp); + + resp.key = a_alarm_control_panel->get_object_id_hash(); + return this->send_alarm_control_panel_state_response(resp); } -bool APIConnection::try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel) { - alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = - reinterpret_cast(v_a_alarm_control_panel); +bool APIConnection::try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { ListEntitiesAlarmControlPanelResponse msg; - msg.key = a_alarm_control_panel->get_object_id_hash(); - msg.object_id = a_alarm_control_panel->get_object_id(); - msg.name = a_alarm_control_panel->get_name(); - msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); - msg.icon = a_alarm_control_panel->get_icon(); - msg.disabled_by_default = a_alarm_control_panel->is_disabled_by_default(); - msg.entity_category = static_cast(a_alarm_control_panel->get_entity_category()); msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); - return api->send_list_entities_alarm_control_panel_response(msg); + msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); + return this->try_send_entity_info_(static_cast(a_alarm_control_panel), msg, + &APIConnection::send_list_entities_alarm_control_panel_response); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); @@ -1680,63 +1384,45 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, std::string event_type) { - if (!APIConnection::try_send_event(this, event, std::move(event_type))) { - this->deferred_message_queue_.defer(event, try_send_event); - } + this->send_state_with_value_(event, &APIConnection::try_send_event_, &APIConnection::try_send_event_, + std::move(event_type)); } void APIConnection::send_event_info(event::Event *event) { - if (!APIConnection::try_send_event_info(this, event)) { - this->deferred_message_queue_.defer(event, try_send_event_info); - } + this->send_info_(static_cast(event), + reinterpret_cast(&APIConnection::try_send_event_info_)); } -bool APIConnection::try_send_event(APIConnection *api, void *v_event) { - event::Event *event = reinterpret_cast(v_event); - return APIConnection::try_send_event(api, event, *(event->last_event_type)); +bool APIConnection::try_send_event_(event::Event *event) { + return this->try_send_event_(event, *(event->last_event_type)); } -bool APIConnection::try_send_event(APIConnection *api, event::Event *event, std::string event_type) { - EventResponse resp{}; - resp.key = event->get_object_id_hash(); +bool APIConnection::try_send_event_(event::Event *event, std::string event_type) { + EventResponse resp; resp.event_type = std::move(event_type); - return api->send_event_response(resp); + + resp.key = event->get_object_id_hash(); + return this->send_event_response(resp); } -bool APIConnection::try_send_event_info(APIConnection *api, void *v_event) { - event::Event *event = reinterpret_cast(v_event); +bool APIConnection::try_send_event_info_(event::Event *event) { ListEntitiesEventResponse msg; - msg.key = event->get_object_id_hash(); - msg.object_id = event->get_object_id(); - if (event->has_own_name()) - msg.name = event->get_name(); - msg.unique_id = get_default_unique_id("event", event); - msg.icon = event->get_icon(); - msg.disabled_by_default = event->is_disabled_by_default(); - msg.entity_category = static_cast(event->get_entity_category()); msg.device_class = event->get_device_class(); for (const auto &event_type : event->get_event_types()) msg.event_types.push_back(event_type); - return api->send_list_entities_event_response(msg); + msg.unique_id = get_default_unique_id("event", event); + return this->try_send_entity_info_(static_cast(event), msg, + &APIConnection::send_list_entities_event_response); } #endif #ifdef USE_UPDATE bool APIConnection::send_update_state(update::UpdateEntity *update) { - if (!this->state_subscription_) - return false; - - if (!APIConnection::try_send_update_state(this, update)) { - this->deferred_message_queue_.defer(update, try_send_update_state); - } - - return true; + return this->send_state_(static_cast(update), + reinterpret_cast(&APIConnection::try_send_update_state_)); } void APIConnection::send_update_info(update::UpdateEntity *update) { - if (!APIConnection::try_send_update_info(this, update)) { - this->deferred_message_queue_.defer(update, try_send_update_info); - } + this->send_info_(static_cast(update), + reinterpret_cast(&APIConnection::try_send_update_info_)); } -bool APIConnection::try_send_update_state(APIConnection *api, void *v_update) { - update::UpdateEntity *update = reinterpret_cast(v_update); - UpdateStateResponse resp{}; - resp.key = update->get_object_id_hash(); +bool APIConnection::try_send_update_state_(update::UpdateEntity *update) { + UpdateStateResponse resp; resp.missing_state = !update->has_state(); if (update->has_state()) { resp.in_progress = update->state == update::UpdateState::UPDATE_STATE_INSTALLING; @@ -1751,21 +1437,15 @@ bool APIConnection::try_send_update_state(APIConnection *api, void *v_update) { resp.release_url = update->update_info.release_url; } - return api->send_update_state_response(resp); + resp.key = update->get_object_id_hash(); + return this->send_update_state_response(resp); } -bool APIConnection::try_send_update_info(APIConnection *api, void *v_update) { - update::UpdateEntity *update = reinterpret_cast(v_update); +bool APIConnection::try_send_update_info_(update::UpdateEntity *update) { ListEntitiesUpdateResponse msg; - msg.key = update->get_object_id_hash(); - msg.object_id = update->get_object_id(); - if (update->has_own_name()) - msg.name = update->get_name(); - msg.unique_id = get_default_unique_id("update", update); - msg.icon = update->get_icon(); - msg.disabled_by_default = update->is_disabled_by_default(); - msg.entity_category = static_cast(update->get_entity_category()); msg.device_class = update->get_device_class(); - return api->send_list_entities_update_response(msg); + msg.unique_id = get_default_unique_id("update", update); + return this->try_send_entity_info_(static_cast(update), msg, + &APIConnection::send_list_entities_update_response); } void APIConnection::update_command(const UpdateCommandRequest &msg) { update::UpdateEntity *update = App.get_update_by_key(msg.key); @@ -1939,26 +1619,29 @@ NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const Nois void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { state_subs_at_ = 0; } -bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { +bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { if (this->remove_) return false; - if (!this->helper_->can_write_without_blocking()) { - delay(0); - APIError err = this->helper_->loop(); - if (err != APIError::OK) { - on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), - api_error_to_str(err), errno); - return false; - } - if (!this->helper_->can_write_without_blocking()) { - // SubscribeLogsResponse - if (message_type != 29) { - ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); - } - delay(0); - return false; - } + if (this->helper_->can_write_without_blocking()) + return true; + delay(0); + APIError err = this->helper_->loop(); + if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + api_error_to_str(err), errno); + return false; + } + if (this->helper_->can_write_without_blocking()) + return true; + if (log_out_of_space) { + ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); + } + return false; +} +bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { + if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse + return false; } APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 1e47418d90..dd24802e72 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -8,13 +8,14 @@ #include "api_server.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include namespace esphome { namespace api { -using send_message_t = bool(APIConnection *, void *); +using send_message_t = bool (APIConnection::*)(void *); /* This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that @@ -30,10 +31,10 @@ class DeferredMessageQueue { protected: void *source_; - send_message_t *send_message_; + send_message_t send_message_; public: - DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {} + DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {} bool operator==(const DeferredMessage &test) const { return (source_ == test.source_ && send_message_ == test.send_message_); } @@ -46,12 +47,13 @@ class DeferredMessageQueue { APIConnection *api_connection_; // helper for allowing only unique entries in the queue - void dmq_push_back_with_dedup_(void *source, send_message_t *send_message); + void dmq_push_back_with_dedup_(void *source, send_message_t send_message); public: DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {} void process_queue(); - void defer(void *source, send_message_t *send_message); + void defer(void *source, send_message_t send_message); + bool empty() const { return deferred_queue_.empty(); } }; class APIConnection : public APIServerConnection { @@ -69,137 +71,213 @@ class APIConnection : public APIServerConnection { #ifdef USE_BINARY_SENSOR bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); - static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor); - static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state); - static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor); + + protected: + bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor); + bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state); + bool try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor); + + public: #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); void send_cover_info(cover::Cover *cover); - static bool try_send_cover_state(APIConnection *api, void *v_cover); - static bool try_send_cover_info(APIConnection *api, void *v_cover); void cover_command(const CoverCommandRequest &msg) override; + + protected: + bool try_send_cover_state_(cover::Cover *cover); + bool try_send_cover_info_(cover::Cover *cover); + + public: #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); void send_fan_info(fan::Fan *fan); - static bool try_send_fan_state(APIConnection *api, void *v_fan); - static bool try_send_fan_info(APIConnection *api, void *v_fan); void fan_command(const FanCommandRequest &msg) override; + + protected: + bool try_send_fan_state_(fan::Fan *fan); + bool try_send_fan_info_(fan::Fan *fan); + + public: #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); void send_light_info(light::LightState *light); - static bool try_send_light_state(APIConnection *api, void *v_light); - static bool try_send_light_info(APIConnection *api, void *v_light); void light_command(const LightCommandRequest &msg) override; + + protected: + bool try_send_light_state_(light::LightState *light); + bool try_send_light_info_(light::LightState *light); + + public: #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor, float state); void send_sensor_info(sensor::Sensor *sensor); - static bool try_send_sensor_state(APIConnection *api, void *v_sensor); - static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state); - static bool try_send_sensor_info(APIConnection *api, void *v_sensor); + + protected: + bool try_send_sensor_state_(sensor::Sensor *sensor); + bool try_send_sensor_state_(sensor::Sensor *sensor, float state); + bool try_send_sensor_info_(sensor::Sensor *sensor); + + public: #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch, bool state); void send_switch_info(switch_::Switch *a_switch); - static bool try_send_switch_state(APIConnection *api, void *v_a_switch); - static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state); - static bool try_send_switch_info(APIConnection *api, void *v_a_switch); void switch_command(const SwitchCommandRequest &msg) override; + + protected: + bool try_send_switch_state_(switch_::Switch *a_switch); + bool try_send_switch_state_(switch_::Switch *a_switch, bool state); + bool try_send_switch_info_(switch_::Switch *a_switch); + + public: #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); void send_text_sensor_info(text_sensor::TextSensor *text_sensor); - static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor); - static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state); - static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor); + + protected: + bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor); + bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state); + bool try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor); + + public: #endif #ifdef USE_ESP32_CAMERA void set_camera_state(std::shared_ptr image); void send_camera_info(esp32_camera::ESP32Camera *camera); - static bool try_send_camera_info(APIConnection *api, void *v_camera); void camera_image(const CameraImageRequest &msg) override; + + protected: + bool try_send_camera_info_(esp32_camera::ESP32Camera *camera); + + public: #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); void send_climate_info(climate::Climate *climate); - static bool try_send_climate_state(APIConnection *api, void *v_climate); - static bool try_send_climate_info(APIConnection *api, void *v_climate); void climate_command(const ClimateCommandRequest &msg) override; + + protected: + bool try_send_climate_state_(climate::Climate *climate); + bool try_send_climate_info_(climate::Climate *climate); + + public: #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number, float state); void send_number_info(number::Number *number); - static bool try_send_number_state(APIConnection *api, void *v_number); - static bool try_send_number_state(APIConnection *api, number::Number *number, float state); - static bool try_send_number_info(APIConnection *api, void *v_number); void number_command(const NumberCommandRequest &msg) override; + + protected: + bool try_send_number_state_(number::Number *number); + bool try_send_number_state_(number::Number *number, float state); + bool try_send_number_info_(number::Number *number); + + public: #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); void send_date_info(datetime::DateEntity *date); - static bool try_send_date_state(APIConnection *api, void *v_date); - static bool try_send_date_info(APIConnection *api, void *v_date); void date_command(const DateCommandRequest &msg) override; + + protected: + bool try_send_date_state_(datetime::DateEntity *date); + bool try_send_date_info_(datetime::DateEntity *date); + + public: #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); void send_time_info(datetime::TimeEntity *time); - static bool try_send_time_state(APIConnection *api, void *v_time); - static bool try_send_time_info(APIConnection *api, void *v_time); void time_command(const TimeCommandRequest &msg) override; + + protected: + bool try_send_time_state_(datetime::TimeEntity *time); + bool try_send_time_info_(datetime::TimeEntity *time); + + public: #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); void send_datetime_info(datetime::DateTimeEntity *datetime); - static bool try_send_datetime_state(APIConnection *api, void *v_datetime); - static bool try_send_datetime_info(APIConnection *api, void *v_datetime); void datetime_command(const DateTimeCommandRequest &msg) override; + + protected: + bool try_send_datetime_state_(datetime::DateTimeEntity *datetime); + bool try_send_datetime_info_(datetime::DateTimeEntity *datetime); + + public: #endif #ifdef USE_TEXT bool send_text_state(text::Text *text, std::string state); void send_text_info(text::Text *text); - static bool try_send_text_state(APIConnection *api, void *v_text); - static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state); - static bool try_send_text_info(APIConnection *api, void *v_text); void text_command(const TextCommandRequest &msg) override; + + protected: + bool try_send_text_state_(text::Text *text); + bool try_send_text_state_(text::Text *text, std::string state); + bool try_send_text_info_(text::Text *text); + + public: #endif #ifdef USE_SELECT bool send_select_state(select::Select *select, std::string state); void send_select_info(select::Select *select); - static bool try_send_select_state(APIConnection *api, void *v_select); - static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state); - static bool try_send_select_info(APIConnection *api, void *v_select); void select_command(const SelectCommandRequest &msg) override; + + protected: + bool try_send_select_state_(select::Select *select); + bool try_send_select_state_(select::Select *select, std::string state); + bool try_send_select_info_(select::Select *select); + + public: #endif #ifdef USE_BUTTON void send_button_info(button::Button *button); - static bool try_send_button_info(APIConnection *api, void *v_button); void button_command(const ButtonCommandRequest &msg) override; + + protected: + bool try_send_button_info_(button::Button *button); + + public: #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock, lock::LockState state); void send_lock_info(lock::Lock *a_lock); - static bool try_send_lock_state(APIConnection *api, void *v_a_lock); - static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state); - static bool try_send_lock_info(APIConnection *api, void *v_a_lock); void lock_command(const LockCommandRequest &msg) override; + + protected: + bool try_send_lock_state_(lock::Lock *a_lock); + bool try_send_lock_state_(lock::Lock *a_lock, lock::LockState state); + bool try_send_lock_info_(lock::Lock *a_lock); + + public: #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); void send_valve_info(valve::Valve *valve); - static bool try_send_valve_state(APIConnection *api, void *v_valve); - static bool try_send_valve_info(APIConnection *api, void *v_valve); void valve_command(const ValveCommandRequest &msg) override; + + protected: + bool try_send_valve_state_(valve::Valve *valve); + bool try_send_valve_info_(valve::Valve *valve); + + public: #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); void send_media_player_info(media_player::MediaPlayer *media_player); - static bool try_send_media_player_state(APIConnection *api, void *v_media_player); - static bool try_send_media_player_info(APIConnection *api, void *v_media_player); void media_player_command(const MediaPlayerCommandRequest &msg) override; + + protected: + bool try_send_media_player_state_(media_player::MediaPlayer *media_player); + bool try_send_media_player_info_(media_player::MediaPlayer *media_player); + + public: #endif bool try_send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { @@ -246,25 +324,37 @@ class APIConnection : public APIServerConnection { #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel); - static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel); void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; + + protected: + bool try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + bool try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + + public: #endif #ifdef USE_EVENT void send_event(event::Event *event, std::string event_type); void send_event_info(event::Event *event); - static bool try_send_event(APIConnection *api, void *v_event); - static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type); - static bool try_send_event_info(APIConnection *api, void *v_event); + + protected: + bool try_send_event_(event::Event *event); + bool try_send_event_(event::Event *event, std::string event_type); + bool try_send_event_info_(event::Event *event); + + public: #endif #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); void send_update_info(update::UpdateEntity *update); - static bool try_send_update_state(APIConnection *api, void *v_update); - static bool try_send_update_info(APIConnection *api, void *v_update); void update_command(const UpdateCommandRequest &msg) override; + + protected: + bool try_send_update_state_(update::UpdateEntity *update); + bool try_send_update_info_(update::UpdateEntity *update); + + public: #endif void on_disconnect_response(const DisconnectResponse &value) override; @@ -318,6 +408,7 @@ class APIConnection : public APIServerConnection { this->proto_write_buffer_.reserve(reserve_size); return {&this->proto_write_buffer_}; } + bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; std::string get_client_combined_info() const { return this->client_combined_info_; } @@ -325,6 +416,99 @@ class APIConnection : public APIServerConnection { protected: friend APIServer; + /** + * Generic send entity state method to reduce code duplication. + * Only attempts to build and send the message if the transmit buffer is available. + * + * This is the base version for entities that use their current state. + * + * @param entity The entity to send state for + * @param try_send_func The function that tries to send the state + * @return True on success or message deferred, false if subscription check failed + */ + bool send_state_(esphome::EntityBase *entity, send_message_t try_send_func) { + if (!this->state_subscription_) + return false; + if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) { + return true; + } + this->deferred_message_queue_.defer(entity, try_send_func); + return true; + } + + /** + * Send entity state method that handles explicit state values. + * Only attempts to build and send the message if the transmit buffer is available. + * + * This method accepts a state parameter to be used instead of the entity's current state. + * It attempts to send the state with the provided value first, and if that fails due to buffer constraints, + * it defers the entity for later processing using the entity-only function. + * + * @tparam EntityT The entity type + * @tparam StateT Type of the state parameter + * @tparam Args Additional argument types (if any) + * @param entity The entity to send state for + * @param try_send_entity_func The function that tries to send the state with entity pointer only + * @param try_send_state_func The function that tries to send the state with entity and state parameters + * @param state The state value to send + * @param args Additional arguments to pass to the try_send_state_func + * @return True on success or message deferred, false if subscription check failed + */ + template + bool send_state_with_value_(EntityT *entity, bool (APIConnection::*try_send_entity_func)(EntityT *), + bool (APIConnection::*try_send_state_func)(EntityT *, StateT, Args...), StateT state, + Args... args) { + if (!this->state_subscription_) + return false; + if (this->try_to_clear_buffer(true) && (this->*try_send_state_func)(entity, state, args...)) { + return true; + } + this->deferred_message_queue_.defer(entity, reinterpret_cast(try_send_entity_func)); + return true; + } + + /** + * Generic send entity info method to reduce code duplication. + * Only attempts to build and send the message if the transmit buffer is available. + * + * @param entity The entity to send info for + * @param try_send_func The function that tries to send the info + */ + void send_info_(esphome::EntityBase *entity, send_message_t try_send_func) { + if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) { + return; + } + this->deferred_message_queue_.defer(entity, try_send_func); + } + + /** + * Generic function for generating entity info response messages. + * This is used to reduce duplication in the try_send_*_info functions. + * + * @param entity The entity to generate info for + * @param response The response object + * @param send_response_func Function pointer to send the response + * @return True if the message was sent successfully + */ + template + bool try_send_entity_info_(esphome::EntityBase *entity, ResponseT &response, + bool (APIServerConnectionBase::*send_response_func)(const ResponseT &)) { + // Set common fields that are shared by all entity types + response.key = entity->get_object_id_hash(); + response.object_id = entity->get_object_id(); + + if (entity->has_own_name()) + response.name = entity->get_name(); + + // Set common EntityBase properties + response.icon = entity->get_icon(); + response.disabled_by_default = entity->is_disabled_by_default(); + response.entity_category = static_cast(entity->get_entity_category()); + + // Send the response using the provided send method + return (this->*send_response_func)(response); + } + bool send_(const void *buf, size_t len, bool force); enum class ConnectionState { From 730441c12081c02b7f656259a0116a9b704fcded Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 15 May 2025 14:26:21 +1200 Subject: [PATCH 106/193] [api] Update api proto to add legacy value (#8802) --- esphome/components/api/api.proto | 3 ++- esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1fdf4e1339..c5c63b8dfc 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -432,7 +432,8 @@ message FanCommandRequest { enum ColorMode { COLOR_MODE_UNKNOWN = 0; COLOR_MODE_ON_OFF = 1; - COLOR_MODE_BRIGHTNESS = 2; + COLOR_MODE_LEGACY_BRIGHTNESS = 2; + COLOR_MODE_BRIGHTNESS = 3; COLOR_MODE_WHITE = 7; COLOR_MODE_COLOR_TEMPERATURE = 11; COLOR_MODE_COLD_WARM_WHITE = 19; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e3181b6166..2d609f6dd6 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -96,6 +96,8 @@ template<> const char *proto_enum_to_string(enums::ColorMode v return "COLOR_MODE_UNKNOWN"; case enums::COLOR_MODE_ON_OFF: return "COLOR_MODE_ON_OFF"; + case enums::COLOR_MODE_LEGACY_BRIGHTNESS: + return "COLOR_MODE_LEGACY_BRIGHTNESS"; case enums::COLOR_MODE_BRIGHTNESS: return "COLOR_MODE_BRIGHTNESS"; case enums::COLOR_MODE_WHITE: diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index c0927ebdc0..1869fc5ba1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -41,7 +41,8 @@ enum FanDirection : uint32_t { enum ColorMode : uint32_t { COLOR_MODE_UNKNOWN = 0, COLOR_MODE_ON_OFF = 1, - COLOR_MODE_BRIGHTNESS = 2, + COLOR_MODE_LEGACY_BRIGHTNESS = 2, + COLOR_MODE_BRIGHTNESS = 3, COLOR_MODE_WHITE = 7, COLOR_MODE_COLOR_TEMPERATURE = 11, COLOR_MODE_COLD_WARM_WHITE = 19, From 1a651ce66de9b8323d62edd753576ba8587040ea Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 15 May 2025 14:40:11 +1200 Subject: [PATCH 107/193] Update some sensor schemas to be Optional (#8803) --- esphome/components/ccs811/sensor.py | 15 ++++++----- esphome/components/dps310/sensor.py | 12 ++++----- esphome/components/ee895/sensor.py | 18 ++++++------- esphome/components/ens160_base/__init__.py | 21 ++++++++------- esphome/components/hte501/sensor.py | 12 ++++----- esphome/components/hyt271/sensor.py | 12 ++++----- esphome/components/mhz19/sensor.py | 18 ++++++++----- esphome/components/ms5611/sensor.py | 12 ++++----- esphome/components/ms8607/sensor.py | 6 ++--- esphome/components/senseair/sensor.py | 6 ++--- esphome/components/sgp30/sensor.py | 30 ++++++++++------------ esphome/components/shtcx/sensor.py | 12 ++++----- esphome/components/t6615/sensor.py | 6 ++--- esphome/components/t6615/t6615.cpp | 3 ++- 14 files changed, 96 insertions(+), 87 deletions(-) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 10565cb328..d9023a415f 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -32,14 +32,14 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(CCS811Component), - cv.Required(CONF_ECO2): sensor.sensor_schema( + cv.Optional(CONF_ECO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_TVOC): sensor.sensor_schema( + cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, @@ -64,10 +64,13 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - sens = await sensor.new_sensor(config[CONF_ECO2]) - cg.add(var.set_co2(sens)) - sens = await sensor.new_sensor(config[CONF_TVOC]) - cg.add(var.set_tvoc(sens)) + if eco2_config := config.get(CONF_ECO2): + sens = await sensor.new_sensor(eco2_config) + cg.add(var.set_co2(sens)) + + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) + cg.add(var.set_tvoc(sens)) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/dps310/sensor.py b/esphome/components/dps310/sensor.py index b2a98e5bab..605812beaa 100644 --- a/esphome/components/dps310/sensor.py +++ b/esphome/components/dps310/sensor.py @@ -27,14 +27,14 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(DPS310Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, icon=ICON_GAUGE, accuracy_decimals=1, @@ -53,10 +53,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py index 0f8f44c8a2..8c9c7e7238 100644 --- a/esphome/components/ee895/sensor.py +++ b/esphome/components/ee895/sensor.py @@ -26,19 +26,19 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(EE895Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, accuracy_decimals=1, device_class=DEVICE_CLASS_PRESSURE, @@ -56,14 +56,14 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py index 28e77e322b..3b6ad8a4ee 100644 --- a/esphome/components/ens160_base/__init__.py +++ b/esphome/components/ens160_base/__init__.py @@ -28,21 +28,21 @@ UNIT_INDEX = "index" CONFIG_SCHEMA_BASE = cv.Schema( { - cv.Required(CONF_ECO2): sensor.sensor_schema( + cv.Optional(CONF_ECO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_TVOC): sensor.sensor_schema( + cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_AQI): sensor.sensor_schema( + cv.Optional(CONF_AQI): sensor.sensor_schema( icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, @@ -62,12 +62,15 @@ async def to_code_base(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - sens = await sensor.new_sensor(config[CONF_ECO2]) - cg.add(var.set_co2(sens)) - sens = await sensor.new_sensor(config[CONF_TVOC]) - cg.add(var.set_tvoc(sens)) - sens = await sensor.new_sensor(config[CONF_AQI]) - cg.add(var.set_aqi(sens)) + if eco2_config := config.get(CONF_ECO2): + sens = await sensor.new_sensor(eco2_config) + cg.add(var.set_co2(sens)) + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) + cg.add(var.set_tvoc(sens)) + if aqi_config := config.get(CONF_AQI): + sens = await sensor.new_sensor(aqi_config) + cg.add(var.set_aqi(sens)) if compensation_config := config.get(CONF_COMPENSATION): sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) diff --git a/esphome/components/hte501/sensor.py b/esphome/components/hte501/sensor.py index 7eef641c4a..17ae3a3d1b 100644 --- a/esphome/components/hte501/sensor.py +++ b/esphome/components/hte501/sensor.py @@ -25,13 +25,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HTE501Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, @@ -49,10 +49,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/hyt271/sensor.py b/esphome/components/hyt271/sensor.py index 22364ce854..bf37646d4f 100644 --- a/esphome/components/hyt271/sensor.py +++ b/esphome/components/hyt271/sensor.py @@ -23,13 +23,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HYT271Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, @@ -47,10 +47,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity(sens)) diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 10428b1e4a..106636a6ba 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MHZ19Component), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, @@ -61,16 +61,20 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_AUTOMATIC_BASELINE_CALIBRATION in config: - cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION])) + if ( + automatic_baseline_calibration := config.get( + CONF_AUTOMATIC_BASELINE_CALIBRATION + ) + ) is not None: + cg.add(var.set_abc_enabled(automatic_baseline_calibration)) cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py index 168ca0c5c8..dfb6083bef 100644 --- a/esphome/components/ms5611/sensor.py +++ b/esphome/components/ms5611/sensor.py @@ -24,13 +24,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MS5611Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, icon=ICON_GAUGE, accuracy_decimals=1, @@ -49,10 +49,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ms8607/sensor.py b/esphome/components/ms8607/sensor.py index 7ed7c61750..038f17190a 100644 --- a/esphome/components/ms8607/sensor.py +++ b/esphome/components/ms8607/sensor.py @@ -29,19 +29,19 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MS8607Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=2, # Resolution: 0.01 device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, accuracy_decimals=2, # Resolution: 0.016 device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=2, # Resolution: 0.04 device_class=DEVICE_CLASS_HUMIDITY, diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index cd6d85c12b..2eb2617e30 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -38,7 +38,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SenseAirComponent), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, @@ -57,8 +57,8 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 8c92f55ef7..848e4e9f9f 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -37,14 +37,14 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SGP30Component), - cv.Required(CONF_ECO2): sensor.sensor_schema( + cv.Optional(CONF_ECO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_TVOC): sensor.sensor_schema( + cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, @@ -86,32 +86,30 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_ECO2 in config: - sens = await sensor.new_sensor(config[CONF_ECO2]) + if eco2_config := config.get(CONF_ECO2): + sens = await sensor.new_sensor(eco2_config) cg.add(var.set_eco2_sensor(sens)) - if CONF_TVOC in config: - sens = await sensor.new_sensor(config[CONF_TVOC]) + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) cg.add(var.set_tvoc_sensor(sens)) - if CONF_ECO2_BASELINE in config: - sens = await sensor.new_sensor(config[CONF_ECO2_BASELINE]) + if eco2_baseline_config := config.get(CONF_ECO2_BASELINE): + sens = await sensor.new_sensor(eco2_baseline_config) cg.add(var.set_eco2_baseline_sensor(sens)) - if CONF_TVOC_BASELINE in config: - sens = await sensor.new_sensor(config[CONF_TVOC_BASELINE]) + if tvoc_baseline_config := config.get(CONF_TVOC_BASELINE): + sens = await sensor.new_sensor(tvoc_baseline_config) cg.add(var.set_tvoc_baseline_sensor(sens)) - if CONF_STORE_BASELINE in config: - cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + if (store_baseline := config.get(CONF_STORE_BASELINE)) is not None: + cg.add(var.set_store_baseline(store_baseline)) - if CONF_BASELINE in config: - baseline_config = config[CONF_BASELINE] + if baseline_config := config.get(CONF_BASELINE): cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE])) cg.add(var.set_tvoc_baseline(baseline_config[CONF_TVOC_BASELINE])) - if CONF_COMPENSATION in config: - compensation_config = config[CONF_COMPENSATION] + if compensation_config := config.get(CONF_COMPENSATION): sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) cg.add(var.set_humidity_sensor(sens)) sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index bb83ee4482..fdb1344fb7 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -26,13 +26,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SHTCXComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, @@ -50,10 +50,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py index 6df40497a9..9315e4a669 100644 --- a/esphome/components/t6615/sensor.py +++ b/esphome/components/t6615/sensor.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(T6615Component), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, @@ -41,6 +41,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index c139c56ce4..1c78833500 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -63,7 +63,8 @@ void T6615Component::loop() { case T6615Command::GET_PPM: { const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]); ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); - this->co2_sensor_->publish_state(ppm); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); break; } default: From 57284b1ac3449b6570ebfc34ac463b1e627cf456 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 23:26:28 -0500 Subject: [PATCH 108/193] Bump cairosvg from 2.8.0 to 2.8.1 (#8799) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 106a6ff901..c89ad4a6e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import esphome-glyphsets==0.2.0 pillow==10.4.0 -cairosvg==2.8.0 +cairosvg==2.8.1 freetype-py==2.5.1 # esp-idf requires this, but doesn't bundle it by default From dd8d8ad95207933c4cfc258c3dc1e33899f83a90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 00:16:08 -0500 Subject: [PATCH 109/193] Use fixed buffer for plaintext protocol like noise protocol (#8800) --- esphome/components/api/api_frame_helper.cpp | 59 +++++++++++++++++---- esphome/components/api/api_frame_helper.h | 14 ++++- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 31b0732275..f251ceb6e4 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -830,6 +830,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // read header while (!rx_header_parsed_) { uint8_t data; + // Reading one byte at a time is fastest in practice for ESP32 when + // there is no data on the wire (which is the common case). + // This results in faster failure detection compared to + // attempting to read multiple bytes at once. ssize_t received = socket_->read(&data, 1); if (received == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { @@ -843,27 +847,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { HELPER_LOG("Connection closed"); return APIError::CONNECTION_CLOSED; } - rx_header_buf_.push_back(data); - // try parse header - if (rx_header_buf_[0] != 0x00) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); - return APIError::BAD_INDICATOR; + // Successfully read a byte + + // Process byte according to current buffer position + if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte) + if (data != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", data); + return APIError::BAD_INDICATOR; + } + // We don't store the indicator byte, just increment position + rx_header_buf_pos_ = 1; // Set to 1 directly + continue; // Need more bytes before we can parse } - size_t i = 1; + // Check buffer overflow before storing + if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed) + state_ = State::FAILED; + HELPER_LOG("Header buffer overflow"); + return APIError::BAD_DATA_PACKET; + } + + // Store byte in buffer (adjust index to account for skipped indicator byte) + rx_header_buf_[rx_header_buf_pos_ - 1] = data; + + // Increment position after storing + rx_header_buf_pos_++; + + // Case 3: If we only have one varint byte, we need more + if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte + continue; // Need more bytes before we can parse + } + + // At this point, we have at least 3 bytes total: + // - Validated indicator byte (0x00) but not stored + // - At least 2 bytes in the buffer for the varints + // Buffer layout: + // First 1-3 bytes: Message size varint (variable length) + // - 2 bytes would only allow up to 16383, which is less than noise's 65535 + // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise + // Remaining 1-2 bytes: Message type varint (variable length) + // We now attempt to parse both varints. If either is incomplete, + // we'll continue reading more bytes. + uint32_t consumed = 0; - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed); if (!msg_size_varint.has_value()) { // not enough data there yet continue; } - i += consumed; rx_header_parsed_len_ = msg_size_varint->as_uint32(); - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed); if (!msg_type_varint.has_value()) { // not enough data there yet continue; @@ -909,7 +946,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // consume msg rx_buf_ = {}; rx_buf_len_ = 0; - rx_header_buf_.clear(); + rx_header_buf_pos_ = 0; rx_header_parsed_ = false; return APIError::OK; } diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 59f3cf7471..db506ea1ce 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -119,6 +119,9 @@ class APINoiseFrameHelper : public APIFrameHelper { std::unique_ptr socket_; std::string info_; + // Fixed-size header buffer for noise protocol: + // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) + // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase uint8_t rx_header_buf_[3]; size_t rx_header_buf_len_ = 0; std::vector rx_buf_; @@ -179,7 +182,16 @@ class APIPlaintextFrameHelper : public APIFrameHelper { std::unique_ptr socket_; std::string info_; - std::vector rx_header_buf_; + // Fixed-size header buffer for plaintext protocol: + // We only need space for the two varints since we validate the indicator byte separately. + // To match noise protocol's maximum message size (65535), we need: + // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint + // + // While varints could theoretically be up to 10 bytes each for 64-bit values, + // attempting to process messages with headers that large would likely crash the + // ESP32 due to memory constraints. + uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type) + uint8_t rx_header_buf_pos_ = 0; bool rx_header_parsed_ = false; uint32_t rx_header_parsed_type_ = 0; uint32_t rx_header_parsed_len_ = 0; From efa6745a5e28313830e94557b4423ff857bed4ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 00:16:25 -0500 Subject: [PATCH 110/193] Optimize protobuf varint decoder for ESPHome use case (#8791) --- esphome/components/api/proto.h | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b8ee6b7920..e110a58eda 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -20,16 +20,26 @@ class ProtoVarInt { explicit ProtoVarInt(uint64_t value) : value_(value) {} static optional parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { - if (consumed != nullptr) - *consumed = 0; - - if (len == 0) + if (len == 0) { + if (consumed != nullptr) + *consumed = 0; return {}; + } - uint64_t result = 0; - uint8_t bitpos = 0; + // Most common case: single-byte varint (values 0-127) + if ((buffer[0] & 0x80) == 0) { + if (consumed != nullptr) + *consumed = 1; + return ProtoVarInt(buffer[0]); + } - for (uint32_t i = 0; i < len; i++) { + // General case for multi-byte varints + // Since we know buffer[0]'s high bit is set, initialize with its value + uint64_t result = buffer[0] & 0x7F; + uint8_t bitpos = 7; + + // Start from the second byte since we've already processed the first + for (uint32_t i = 1; i < len; i++) { uint8_t val = buffer[i]; result |= uint64_t(val & 0x7F) << uint64_t(bitpos); bitpos += 7; @@ -40,7 +50,9 @@ class ProtoVarInt { } } - return {}; + if (consumed != nullptr) + *consumed = 0; + return {}; // Incomplete or invalid varint } uint32_t as_uint32() const { return this->value_; } From 0b77cb1d1692f036db2f99cc73940cc16b7302f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 04:36:28 -0500 Subject: [PATCH 111/193] Logger Recursion Guard per Task on ESP32 (#8765) --- esphome/components/logger/__init__.py | 1 + esphome/components/logger/logger.cpp | 47 +++++++++++---------- esphome/components/logger/logger.h | 60 ++++++++++++++++++++++++--- 3 files changed, 82 insertions(+), 26 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 01e75a424d..4698c1d9f1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -254,6 +254,7 @@ async def to_code(config): config[CONF_TX_BUFFER_SIZE], ) if CORE.is_esp32: + cg.add(log.create_pthread_key()) task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 812a7cc16d..0ad909cb07 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -14,20 +14,27 @@ namespace logger { static const char *const TAG = "logger"; #ifdef USE_ESP32 -// Implementation for ESP32 (multi-core with atomic support) -// Main thread: synchronous logging with direct buffer access -// Other threads: console output with stack buffer, callbacks via async buffer +// Implementation for ESP32 (multi-task platform with task-specific tracking) +// Main task always uses direct buffer access for console output and callbacks +// Other tasks: +// - With task log buffer: stack buffer for console output, async buffer for callbacks +// - Without task log buffer: only console output, no callbacks void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed)) + if (level > this->level_for(tag)) return; - recursion_guard_.store(true, std::memory_order_relaxed); TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); + bool is_main_task = (current_task == main_task_); - // For main task: call log_message_to_buffer_and_send_ which does console and callback logging - if (current_task == main_task_) { + // Check and set recursion guard - uses pthread TLS for per-task state + if (this->check_and_set_task_log_recursion_(is_main_task)) { + return; // Recursion detected + } + + // Main task uses the shared buffer for efficiency + if (is_main_task) { this->log_message_to_buffer_and_send_(level, tag, line, format, args); - recursion_guard_.store(false, std::memory_order_release); + this->reset_task_log_recursion_(is_main_task); return; } @@ -51,23 +58,21 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * } #endif // USE_ESPHOME_TASK_LOG_BUFFER - recursion_guard_.store(false, std::memory_order_release); + // Reset the recursion guard for this task + this->reset_task_log_recursion_(is_main_task); } -#endif // USE_ESP32 - -#ifndef USE_ESP32 -// Implementation for platforms that do not support atomic operations -// or have to consider logging in other tasks +#else +// Implementation for all other platforms void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag) || recursion_guard_) + if (level > this->level_for(tag) || global_recursion_guard_) return; - recursion_guard_ = true; + global_recursion_guard_ = true; // Format and send to both console and callbacks this->log_message_to_buffer_and_send_(level, tag, line, format, args); - recursion_guard_ = false; + global_recursion_guard_ = false; } #endif // !USE_ESP32 @@ -76,10 +81,10 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT - if (level > this->level_for(tag) || recursion_guard_) + if (level > this->level_for(tag) || global_recursion_guard_) return; - recursion_guard_ = true; + global_recursion_guard_ = true; this->tx_buffer_at_ = 0; // Copy format string from progmem @@ -91,7 +96,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr // Buffer full from copying format if (this->tx_buffer_at_ >= this->tx_buffer_size_) { - recursion_guard_ = false; // Make sure to reset the recursion guard before returning + global_recursion_guard_ = false; // Make sure to reset the recursion guard before returning return; } @@ -107,7 +112,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); - recursion_guard_ = false; + global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8619cc0992..5c53c4d40c 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -3,7 +3,7 @@ #include #include #ifdef USE_ESP32 -#include +#include #endif #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -84,6 +84,23 @@ enum UARTSelection { }; #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY +/** + * @brief Logger component for all ESPHome logging. + * + * This class implements a multi-platform logging system with protection against recursion. + * + * Recursion Protection Strategy: + * - On ESP32: Uses task-specific recursion guards + * * Main task: Uses a dedicated boolean member variable for efficiency + * * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state + * - On other platforms: Uses a simple global recursion guard + * + * We use pthread TLS via pthread_key_create to create a unique key for storing + * task-specific recursion state, which: + * 1. Efficiently handles multiple tasks without locks or mutexes + * 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables + * 3. Avoids the limitations of the fixed FreeRTOS task local storage slots + */ class Logger : public Component { public: explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); @@ -102,6 +119,9 @@ class Logger : public Component { #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } #endif +#ifdef USE_ESP32 + void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. @@ -222,18 +242,22 @@ class Logger : public Component { std::map log_levels_{}; CallbackManager log_callback_{}; int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#ifdef USE_ESP32 - std::atomic recursion_guard_{false}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer #endif +#ifdef USE_ESP32 + // Task-specific recursion guards: + // - Main task uses a dedicated member variable for efficiency + // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create + bool main_task_recursion_guard_{false}; + pthread_key_t log_recursion_key_; #else - bool recursion_guard_{false}; + bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - void *main_task_ = nullptr; CallbackManager level_callback_{}; #if defined(USE_ESP32) || defined(USE_LIBRETINY) + void *main_task_ = nullptr; // Only used for thread name identification const char *HOT get_thread_name_() { TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); if (current_task == main_task_) { @@ -248,6 +272,32 @@ class Logger : public Component { } #endif +#ifdef USE_ESP32 + inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) { + if (is_main_task) { + const bool was_recursive = main_task_recursion_guard_; + main_task_recursion_guard_ = true; + return was_recursive; + } + + intptr_t current = (intptr_t) pthread_getspecific(log_recursion_key_); + if (current != 0) + return true; + + pthread_setspecific(log_recursion_key_, (void *) 1); + return false; + } + + inline void HOT reset_task_log_recursion_(bool is_main_task) { + if (is_main_task) { + main_task_recursion_guard_ = false; + return; + } + + pthread_setspecific(log_recursion_key_, (void *) 0); + } +#endif + inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, int *buffer_at, int buffer_size) { // Format header From 88edddf07a6f471fa19d43d35330beff6d184762 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Thu, 15 May 2025 11:45:07 +0200 Subject: [PATCH 112/193] [log] improve/refactor `log` (#8708) --- esphome/__main__.py | 30 +++++++++-------- esphome/config.py | 26 +++++++-------- esphome/log.py | 34 +++++++++---------- esphome/mqtt.py | 4 +-- esphome/wizard.py | 80 +++++++++++++++++++++++++-------------------- 5 files changed, 91 insertions(+), 83 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c78eda7e12..9f638456e6 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -43,7 +43,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, coroutine from esphome.helpers import get_bool_env, indent, is_ip_address -from esphome.log import Fore, color, setup_log +from esphome.log import AnsiFore, color, setup_log from esphome.util import ( get_serial_ports, list_yaml_files, @@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None): raise ValueError break except ValueError: - safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) + safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'")) return options[opt - 1][1] @@ -596,30 +596,30 @@ def command_update_all(args): click.echo(f"{half_line}{middle_text}{half_line}") for f in files: - print(f"Updating {color(Fore.CYAN, f)}") + print(f"Updating {color(AnsiFore.CYAN, f)}") print("-" * twidth) print() rc = run_external_process( "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" ) if rc == 0: - print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") + print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}") success[f] = True else: - print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") + print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}") success[f] = False print() print() print() - print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") + print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]") failed = 0 for f in files: if success[f]: - print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}") + print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}") else: - print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}") + print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}") failed += 1 return failed @@ -645,7 +645,7 @@ def command_rename(args, config): if c not in ALLOWED_NAME_CHARS: print( color( - Fore.BOLD_RED, + AnsiFore.BOLD_RED, f"'{c}' is an invalid character for names. Valid characters are: " f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", ) @@ -658,7 +658,9 @@ def command_rename(args, config): yaml = yaml_util.load_yaml(CORE.config_path) if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: print( - color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") + color( + AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed." + ) ) return 1 old_name = yaml[CONF_ESPHOME][CONF_NAME] @@ -681,7 +683,7 @@ def command_rename(args, config): ) > 1 ): - print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) + print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename")) return 1 new_raw = re.sub( @@ -693,7 +695,7 @@ def command_rename(args, config): new_path = os.path.join(CORE.config_dir, args.name + ".yaml") print( - f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" + f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}" ) print() @@ -702,7 +704,7 @@ def command_rename(args, config): rc = run_external_process("esphome", "config", new_path) if rc != 0: - print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes.")) os.remove(new_path) return 1 @@ -728,7 +730,7 @@ def command_rename(args, config): if CORE.config_path != new_path: os.remove(CORE.config_path) - print(color(Fore.BOLD_GREEN, "SUCCESS")) + print(color(AnsiFore.BOLD_GREEN, "SUCCESS")) print() return 0 diff --git a/esphome/config.py b/esphome/config.py index 09ee2a8f9b..4b26b33c78 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -28,7 +28,7 @@ import esphome.core.config as core_config import esphome.final_validate as fv from esphome.helpers import indent from esphome.loader import ComponentManifest, get_component, get_platform -from esphome.log import Fore, color +from esphome.log import AnsiFore, color from esphome.types import ConfigFragmentType, ConfigType from esphome.util import OrderedDict, safe_print from esphome.voluptuous_schema import ExtraKeysInvalid @@ -959,7 +959,7 @@ def line_info(config, path, highlight=True): if obj: mark = obj.start_mark source = f"[source {mark.document}:{mark.line + 1}]" - return color(Fore.CYAN, source) + return color(AnsiFore.CYAN, source) return "None" @@ -983,7 +983,7 @@ def dump_dict( if at_root: error = config.get_error_for_path(path) if error is not None: - ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" + ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n" if isinstance(conf, (list, tuple)): multiline = True @@ -995,11 +995,11 @@ def dump_dict( path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" + ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n" sep = "- " if config.is_in_error_path(path_): - sep = color(Fore.RED, sep) + sep = color(AnsiFore.RED, sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) @@ -1018,11 +1018,11 @@ def dump_dict( path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" + ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n" st = f"{k}: " if config.is_in_error_path(path_): - st = color(Fore.RED, st) + st = color(AnsiFore.RED, st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) @@ -1044,7 +1044,7 @@ def dump_dict( if len(conf) > 80: conf = f"|-\n{indent(conf)}" error = config.get_error_for_path(path) - col = Fore.BOLD_RED if error else Fore.KEEP + col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): @@ -1052,13 +1052,13 @@ def dump_dict( conf = f"!lambda |-\n{indent(str(conf.value))}" error = config.get_error_for_path(path) - col = Fore.BOLD_RED if error else Fore.KEEP + col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP ret += color(col, conf) elif conf is None: pass else: error = config.get_error_for_path(path) - col = Fore.BOLD_RED if error else Fore.KEEP + col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP ret += color(col, str(conf)) multiline = "\n" in ret @@ -1100,13 +1100,13 @@ def read_config(command_line_substitutions): if not CORE.verbose: res = strip_default_ids(res) - safe_print(color(Fore.BOLD_RED, "Failed config")) + safe_print(color(AnsiFore.BOLD_RED, "Failed config")) safe_print("") for path, domain in res.output_paths: if not res.is_in_error_path(path): continue - errstr = color(Fore.BOLD_RED, f"{domain}:") + errstr = color(AnsiFore.BOLD_RED, f"{domain}:") errline = line_info(res, path) if errline: errstr += f" {errline}" @@ -1121,7 +1121,7 @@ def read_config(command_line_substitutions): safe_print(indent("\n".join(split_dump[:i]))) for err in res.errors: - safe_print(color(Fore.BOLD_RED, err.msg)) + safe_print(color(AnsiFore.BOLD_RED, err.msg)) safe_print("") return None diff --git a/esphome/log.py b/esphome/log.py index 516f27be45..7e69a2fef8 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -1,9 +1,10 @@ +from enum import Enum import logging from esphome.core import CORE -class AnsiFore: +class AnsiFore(Enum): KEEP = "" BLACK = "\033[30m" RED = "\033[31m" @@ -26,7 +27,7 @@ class AnsiFore: BOLD_RESET = "\033[1;39m" -class AnsiStyle: +class AnsiStyle(Enum): BRIGHT = "\033[1m" BOLD = "\033[1m" DIM = "\033[2m" @@ -35,16 +36,10 @@ class AnsiStyle: RESET_ALL = "\033[0m" -Fore = AnsiFore() -Style = AnsiStyle() - - -def color(col: str, msg: str, reset: bool = True) -> bool: - if col and not col.startswith("\033["): - raise ValueError("Color must be value from esphome.log.Fore") - s = str(col) + msg +def color(col: AnsiFore, msg: str, reset: bool = True) -> str: + s = col.value + msg if reset and col: - s += str(Style.RESET_ALL) + s += AnsiStyle.RESET_ALL.value return s @@ -54,20 +49,21 @@ class ESPHomeLogFormatter(logging.Formatter): fmt += "%(levelname)s %(message)s" super().__init__(fmt=fmt, style="%") - def format(self, record): + # @override + def format(self, record: logging.LogRecord) -> str: formatted = super().format(record) prefix = { - "DEBUG": Fore.CYAN, - "INFO": Fore.GREEN, - "WARNING": Fore.YELLOW, - "ERROR": Fore.RED, - "CRITICAL": Fore.RED, + "DEBUG": AnsiFore.CYAN.value, + "INFO": AnsiFore.GREEN.value, + "WARNING": AnsiFore.YELLOW.value, + "ERROR": AnsiFore.RED.value, + "CRITICAL": AnsiFore.RED.value, }.get(record.levelname, "") - return f"{prefix}{formatted}{Style.RESET_ALL}" + return f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}" def setup_log( - log_level=logging.INFO, + log_level: int = logging.INFO, include_timestamp: bool = False, ) -> None: import colorama diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 2403a4a1d9..a420b5ba7f 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -28,7 +28,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import Fore, color +from esphome.log import AnsiFore, color from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -291,7 +291,7 @@ def get_fingerprint(config): sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}") + safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") safe_print( f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" ) diff --git a/esphome/wizard.py b/esphome/wizard.py index 8c5bd07e1f..ca987304e2 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -9,7 +9,7 @@ import esphome.config_validation as cv from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD from esphome.core import CORE from esphome.helpers import get_bool_env, write_file -from esphome.log import Fore, color +from esphome.log import AnsiFore, color from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_input, safe_print @@ -219,7 +219,7 @@ def wizard_write(path, **kwargs): elif board in rtl87xx_boards.BOARDS: platform = "RTL87XX" else: - safe_print(color(Fore.RED, f'The board "{board}" is unknown.')) + safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.')) return False kwargs["platform"] = platform hardware = kwargs["platform"] @@ -274,12 +274,12 @@ def wizard(path): if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( - f"Please make your configuration file {color(Fore.CYAN, path)} have the extension .yaml or .yml" + f"Please make your configuration file {color(AnsiFore.CYAN, path)} have the extension .yaml or .yml" ) return 1 if os.path.exists(path): safe_print( - f"Uh oh, it seems like {color(Fore.CYAN, path)} already exists, please delete that file first or chose another configuration file." + f"Uh oh, it seems like {color(AnsiFore.CYAN, path)} already exists, please delete that file first or chose another configuration file." ) return 2 @@ -298,17 +298,19 @@ def wizard(path): sleep(3.0) safe_print() safe_print_step(1, CORE_BIG) - safe_print(f"First up, please choose a {color(Fore.GREEN, 'name')} for your node.") + safe_print( + f"First up, please choose a {color(AnsiFore.GREEN, 'name')} for your node." + ) safe_print( "It should be a unique name that can be used to identify the device later." ) sleep(1) safe_print( - f"For example, I like calling the node in my living room {color(Fore.BOLD_WHITE, 'livingroom')}." + f"For example, I like calling the node in my living room {color(AnsiFore.BOLD_WHITE, 'livingroom')}." ) safe_print() sleep(1) - name = safe_input(color(Fore.BOLD_WHITE, "(name): ")) + name = safe_input(color(AnsiFore.BOLD_WHITE, "(name): ")) while True: try: @@ -317,7 +319,7 @@ def wizard(path): except vol.Invalid: safe_print( color( - Fore.RED, + AnsiFore.RED, f'Oh noes, "{name}" isn\'t a valid name. Names can only ' f"include numbers, lower-case letters and hyphens. ", ) @@ -325,11 +327,13 @@ def wizard(path): name = strip_accents(name).lower().replace(" ", "-") name = strip_accents(name).lower().replace("_", "-") name = "".join(c for c in name if c in ALLOWED_NAME_CHARS) - safe_print(f'Shall I use "{color(Fore.CYAN, name)}" as the name instead?') + safe_print( + f'Shall I use "{color(AnsiFore.CYAN, name)}" as the name instead?' + ) sleep(0.5) name = default_input("(name [{}]): ", name) - safe_print(f'Great! Your node is now called "{color(Fore.CYAN, name)}".') + safe_print(f'Great! Your node is now called "{color(AnsiFore.CYAN, name)}".') sleep(1) safe_print_step(2, ESP_BIG) safe_print( @@ -346,7 +350,7 @@ def wizard(path): sleep(0.5) safe_print() platform = safe_input( - color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ") + color(AnsiFore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ") ) try: platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper()) @@ -355,7 +359,9 @@ def wizard(path): safe_print( f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.' ) - safe_print(f"Thanks! You've chosen {color(Fore.CYAN, platform)} as your platform.") + safe_print( + f"Thanks! You've chosen {color(AnsiFore.CYAN, platform)} as your platform." + ) safe_print() sleep(1) @@ -376,27 +382,29 @@ def wizard(path): else: raise NotImplementedError("Unknown platform!") - safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.") + safe_print( + f"Next, I need to know what {color(AnsiFore.GREEN, 'board')} you're using." + ) sleep(0.5) - safe_print(f"Please go to {color(Fore.GREEN, board_link)} and choose a board.") + safe_print(f"Please go to {color(AnsiFore.GREEN, board_link)} and choose a board.") if platform == "ESP32": - safe_print(f"(Type {color(Fore.GREEN, 'esp01_1m')} for Sonoff devices)") + safe_print(f"(Type {color(AnsiFore.GREEN, 'esp01_1m')} for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link if platform == "ESP32": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcu-32s")}".') boards_list = esp32_boards.BOARDS.items() elif platform == "ESP8266": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcuv2")}".') boards_list = esp8266_boards.BOARDS.items() elif platform == "BK72XX": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".') boards_list = bk72xx_boards.BOARDS.items() elif platform == "RTL87XX": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".') boards_list = rtl87xx_boards.BOARDS.items() elif platform == "RP2040": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "rpipicow")}".') boards_list = rp2040_boards.BOARDS.items() else: @@ -409,19 +417,21 @@ def wizard(path): boards.append(board_id) while True: - board = safe_input(color(Fore.BOLD_WHITE, "(board): ")) + board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: safe_print( - color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.') + color( + AnsiFore.RED, f'Sorry, I don\'t think the board "{board}" exists.' + ) ) safe_print() sleep(0.25) safe_print() - safe_print(f"Way to go! You've chosen {color(Fore.CYAN, board)} as your board.") + safe_print(f"Way to go! You've chosen {color(AnsiFore.CYAN, board)} as your board.") safe_print() sleep(1) @@ -432,19 +442,19 @@ def wizard(path): safe_print() sleep(1) safe_print( - f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" + f"First, what's the {color(AnsiFore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" ) sleep(1.5) - safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "Abraham Linksys")}".') while True: - ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): ")) + ssid = safe_input(color(AnsiFore.BOLD_WHITE, "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: safe_print( color( - Fore.RED, + AnsiFore.RED, f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.', ) ) @@ -452,18 +462,18 @@ def wizard(path): sleep(1) safe_print( - f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.' + f'Thank you very much! You\'ve just chosen "{color(AnsiFore.CYAN, ssid)}" as your SSID.' ) safe_print() sleep(0.75) safe_print( - f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" + f"Now please state the {color(AnsiFore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" ) safe_print() - safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "PASSWORD42")}"') sleep(0.5) - psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): ")) + psk = safe_input(color(AnsiFore.BOLD_WHITE, "(PSK): ")) safe_print( "Perfect! WiFi is now set up (you can create static IPs and so on later)." ) @@ -475,12 +485,12 @@ def wizard(path): "(over the air) and integrates into Home Assistant with a native API." ) safe_print( - f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?" + f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?" ) safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = safe_input(color(Fore.BOLD_WHITE, "(password): ")) + password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) else: ssid, password, psk = "", "", "" @@ -497,8 +507,8 @@ def wizard(path): safe_print() safe_print( - color(Fore.CYAN, "DONE! I've now written a new configuration file to ") - + color(Fore.BOLD_CYAN, path) + color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ") + + color(AnsiFore.BOLD_CYAN, path) ) safe_print() safe_print("Next steps:") From 4761ffe0235e295df9a5c07247cf7da78150d36d Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Thu, 15 May 2025 12:07:41 +0200 Subject: [PATCH 113/193] [gps] update lib, improve code/tests/config (#8768) --- CODEOWNERS | 2 +- esphome/components/gps/__init__.py | 53 +++++++++++++++++++++--------- esphome/components/gps/gps.cpp | 50 ++++++++++++++++++---------- esphome/components/gps/gps.h | 31 ++++++++--------- platformio.ini | 2 +- tests/components/gps/common.yaml | 14 ++++++++ 6 files changed, 102 insertions(+), 50 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ddd0494a3c..a6e08f225d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,7 +169,7 @@ esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gpio/one_wire/* @ssieb -esphome/components/gps/* @coogle +esphome/components/gps/* @coogle @ximex esphome/components/graph/* @synco esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 88e6f0fd9b..7ccd82dec3 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -9,23 +9,32 @@ from esphome.const import ( CONF_LONGITUDE, CONF_SATELLITES, CONF_SPEED, + DEVICE_CLASS_SPEED, STATE_CLASS_MEASUREMENT, UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, ) +CONF_GPS_ID = "gps_id" +CONF_HDOP = "hdop" + +ICON_ALTIMETER = "mdi:altimeter" +ICON_COMPASS = "mdi:compass" +ICON_LATITUDE = "mdi:latitude" +ICON_LONGITUDE = "mdi:longitude" +ICON_SATELLITE = "mdi:satellite-variant" +ICON_SPEEDOMETER = "mdi:speedometer" + DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor"] -CODEOWNERS = ["@coogle"] +CODEOWNERS = ["@coogle", "@ximex"] gps_ns = cg.esphome_ns.namespace("gps") GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) GPSListener = gps_ns.class_("GPSListener") -CONF_GPS_ID = "gps_id" -CONF_HDOP = "hdop" MULTI_CONF = True CONFIG_SCHEMA = cv.All( cv.Schema( @@ -33,25 +42,37 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(GPS), cv.Optional(CONF_LATITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, + icon=ICON_LATITUDE, accuracy_decimals=6, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, + icon=ICON_LONGITUDE, accuracy_decimals=6, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + icon=ICON_SPEEDOMETER, accuracy_decimals=3, + device_class=DEVICE_CLASS_SPEED, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, + icon=ICON_COMPASS, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, + icon=ICON_ALTIMETER, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + icon=ICON_SATELLITE, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), @@ -73,28 +94,28 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_LATITUDE in config: - sens = await sensor.new_sensor(config[CONF_LATITUDE]) + if latitude_config := config.get(CONF_LATITUDE): + sens = await sensor.new_sensor(latitude_config) cg.add(var.set_latitude_sensor(sens)) - if CONF_LONGITUDE in config: - sens = await sensor.new_sensor(config[CONF_LONGITUDE]) + if longitude_config := config.get(CONF_LONGITUDE): + sens = await sensor.new_sensor(longitude_config) cg.add(var.set_longitude_sensor(sens)) - if CONF_SPEED in config: - sens = await sensor.new_sensor(config[CONF_SPEED]) + if speed_config := config.get(CONF_SPEED): + sens = await sensor.new_sensor(speed_config) cg.add(var.set_speed_sensor(sens)) - if CONF_COURSE in config: - sens = await sensor.new_sensor(config[CONF_COURSE]) + if course_config := config.get(CONF_COURSE): + sens = await sensor.new_sensor(course_config) cg.add(var.set_course_sensor(sens)) - if CONF_ALTITUDE in config: - sens = await sensor.new_sensor(config[CONF_ALTITUDE]) + if altitude_config := config.get(CONF_ALTITUDE): + sens = await sensor.new_sensor(altitude_config) cg.add(var.set_altitude_sensor(sens)) - if CONF_SATELLITES in config: - sens = await sensor.new_sensor(config[CONF_SATELLITES]) + if satellites_config := config.get(CONF_SATELLITES): + sens = await sensor.new_sensor(satellites_config) cg.add(var.set_satellites_sensor(sens)) if hdop_config := config.get(CONF_HDOP): @@ -102,4 +123,4 @@ async def to_code(config): cg.add(var.set_hdop_sensor(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus - cg.add_library("mikalhart/TinyGPSPlus", "1.0.2") + cg.add_library("mikalhart/TinyGPSPlus", "1.1.0") diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index e54afdb07e..9dcb351b39 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -10,6 +10,17 @@ static const char *const TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::dump_config() { + ESP_LOGCONFIG(TAG, "GPS:"); + LOG_SENSOR(" ", "Latitude", this->latitude_sensor_); + LOG_SENSOR(" ", "Longitude", this->longitude_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Course", this->course_sensor_); + LOG_SENSOR(" ", "Altitude", this->altitude_sensor_); + LOG_SENSOR(" ", "Satellites", this->satellites_sensor_); + LOG_SENSOR(" ", "HDOP", this->hdop_sensor_); +} + void GPS::update() { if (this->latitude_sensor_ != nullptr) this->latitude_sensor_->publish_state(this->latitude_); @@ -34,40 +45,45 @@ void GPS::update() { } void GPS::loop() { - while (this->available() && !this->has_time_) { + while (this->available() > 0 && !this->has_time_) { if (this->tiny_gps_.encode(this->read())) { - if (tiny_gps_.location.isUpdated()) { - this->latitude_ = tiny_gps_.location.lat(); - this->longitude_ = tiny_gps_.location.lng(); + if (this->tiny_gps_.location.isUpdated()) { + this->latitude_ = this->tiny_gps_.location.lat(); + this->longitude_ = this->tiny_gps_.location.lng(); ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %f", this->latitude_); - ESP_LOGD(TAG, " Lon: %f", this->longitude_); + ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_); + ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_); } - if (tiny_gps_.speed.isUpdated()) { - this->speed_ = tiny_gps_.speed.kmph(); + if (this->tiny_gps_.speed.isUpdated()) { + this->speed_ = this->tiny_gps_.speed.kmph(); ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); } - if (tiny_gps_.course.isUpdated()) { - this->course_ = tiny_gps_.course.deg(); + + if (this->tiny_gps_.course.isUpdated()) { + this->course_ = this->tiny_gps_.course.deg(); ESP_LOGD(TAG, "Course: %.2f °", this->course_); } - if (tiny_gps_.altitude.isUpdated()) { - this->altitude_ = tiny_gps_.altitude.meters(); + + if (this->tiny_gps_.altitude.isUpdated()) { + this->altitude_ = this->tiny_gps_.altitude.meters(); ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); } - if (tiny_gps_.satellites.isUpdated()) { - this->satellites_ = tiny_gps_.satellites.value(); + + if (this->tiny_gps_.satellites.isUpdated()) { + this->satellites_ = this->tiny_gps_.satellites.value(); ESP_LOGD(TAG, "Satellites: %d", this->satellites_); } - if (tiny_gps_.hdop.isUpdated()) { - this->hdop_ = tiny_gps_.hdop.hdop(); + + if (this->tiny_gps_.hdop.isUpdated()) { + this->hdop_ = this->tiny_gps_.hdop.hdop(); ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_); } - for (auto *listener : this->listeners_) + for (auto *listener : this->listeners_) { listener->on_update(this->tiny_gps_); + } } } } diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index a400820738..7bc23ed1e0 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -5,7 +5,7 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" -#include +#include #include @@ -27,13 +27,13 @@ class GPSListener { class GPS : public PollingComponent, public uart::UARTDevice { public: - void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; } - void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; } - void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; } - void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } - void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } - void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } - void set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; } + void set_latitude_sensor(sensor::Sensor *latitude_sensor) { this->latitude_sensor_ = latitude_sensor; } + void set_longitude_sensor(sensor::Sensor *longitude_sensor) { this->longitude_sensor_ = longitude_sensor; } + void set_speed_sensor(sensor::Sensor *speed_sensor) { this->speed_sensor_ = speed_sensor; } + void set_course_sensor(sensor::Sensor *course_sensor) { this->course_sensor_ = course_sensor; } + void set_altitude_sensor(sensor::Sensor *altitude_sensor) { this->altitude_sensor_ = altitude_sensor; } + void set_satellites_sensor(sensor::Sensor *satellites_sensor) { this->satellites_sensor_ = satellites_sensor; } + void set_hdop_sensor(sensor::Sensor *hdop_sensor) { this->hdop_sensor_ = hdop_sensor; } void register_listener(GPSListener *listener) { listener->parent_ = this; @@ -41,19 +41,20 @@ class GPS : public PollingComponent, public uart::UARTDevice { } float get_setup_priority() const override { return setup_priority::HARDWARE; } + void dump_config() override; void loop() override; void update() override; TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: - float latitude_ = NAN; - float longitude_ = NAN; - float speed_ = NAN; - float course_ = NAN; - float altitude_ = NAN; - int satellites_ = 0; - double hdop_ = NAN; + float latitude_{NAN}; + float longitude_{NAN}; + float speed_{NAN}; + float course_{NAN}; + float altitude_{NAN}; + uint16_t satellites_{0}; + float hdop_{NAN}; sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr}; diff --git a/platformio.ini b/platformio.ini index ccfd52c3ca..292188c6fa 100644 --- a/platformio.ini +++ b/platformio.ini @@ -64,7 +64,7 @@ lib_deps = heman/AsyncMqttClient-esphome@1.0.0 ; mqtt esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base - mikalhart/TinyGPSPlus@1.0.2 ; gps + mikalhart/TinyGPSPlus@1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr diff --git a/tests/components/gps/common.yaml b/tests/components/gps/common.yaml index fc8228c909..53dc67e457 100644 --- a/tests/components/gps/common.yaml +++ b/tests/components/gps/common.yaml @@ -6,6 +6,20 @@ uart: parity: EVEN gps: + latitude: + name: "Latitude" + longitude: + name: "Longitude" + altitude: + name: "Altitude" + speed: + name: "Speed" + course: + name: "Course" + satellites: + name: "Satellites" + hdop: + name: "HDOP" time: - platform: gps From 115975c409cbe364b2c49b873f96a757cfe36804 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 15:44:47 -0500 Subject: [PATCH 114/193] Bump aioesphomeapi from 31.0.0 to 31.0.1 (#8809) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c89ad4a6e4..ae4f9d1558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==31.0.0 +aioesphomeapi==31.0.1 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import From d9b860088e2247bcb1c85c91b1986e83771b4871 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 15:45:01 -0500 Subject: [PATCH 115/193] Bump setuptools from 80.4.0 to 80.7.1 (#8808) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1971f033c8..60f1638975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.4.0", "wheel>=0.43,<0.46"] +requires = ["setuptools==80.7.1", "wheel>=0.43,<0.46"] build-backend = "setuptools.build_meta" [project] From 7965558d5e707d81718eea3e735cae22243e2d1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 18:42:54 -0500 Subject: [PATCH 116/193] Fix ESP32 Camera class inheritance (#8811) --- esphome/components/esp32_camera/esp32_camera.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 71f47d3c06..d5fe48c2a7 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -106,7 +106,7 @@ class CameraImageReader { }; /* ---------------- ESP32Camera class ---------------- */ -class ESP32Camera : public Component, public EntityBase { +class ESP32Camera : public EntityBase, public Component { public: ESP32Camera(); From 218f8e0cafd3bd5ae35889852ce6e1cbaf2f4c16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 15:42:44 -0400 Subject: [PATCH 117/193] Bump ruff from 0.11.9 to 0.11.10 (#8818) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6dd8d883ba..863c320d96 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.2.0 # also change in .pre-commit-config.yaml when updating -ruff==0.11.9 # also change in .pre-commit-config.yaml when updating +ruff==0.11.10 # also change in .pre-commit-config.yaml when updating pyupgrade==3.19.1 # also change in .pre-commit-config.yaml when updating pre-commit From b469a504e46649c6f51783ae4a5df98cae63f4e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 15:42:51 -0400 Subject: [PATCH 118/193] Bump cairosvg from 2.8.1 to 2.8.2 (#8817) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae4f9d1558..d867610c5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import esphome-glyphsets==0.2.0 pillow==10.4.0 -cairosvg==2.8.1 +cairosvg==2.8.2 freetype-py==2.5.1 # esp-idf requires this, but doesn't bundle it by default From 5c92367ca23470d8f2e0d7d71ca54cf2febaa659 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 17 May 2025 09:41:19 +1000 Subject: [PATCH 119/193] [script] Use local import for zephyr (#8822) --- script/helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 3c1b0c0ddd..1a0349e434 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -5,7 +5,6 @@ import re import subprocess import colorama -import helpers_zephyr root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") @@ -149,7 +148,9 @@ def load_idedata(environment): Path(temp_folder).mkdir(exist_ok=True) if "nrf" in environment: - data = helpers_zephyr.load_idedata(environment, temp_folder, platformio_ini) + from helpers_zephyr import load_idedata as zephyr_load_idedata + + data = zephyr_load_idedata(environment, temp_folder, platformio_ini) else: stdout = subprocess.check_output( ["pio", "run", "-t", "idedata", "-e", environment] From fefcb45e1f569fb7eb9f3bbd7c27446436c98562 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 May 2025 22:50:06 -0400 Subject: [PATCH 120/193] Bump cryptography to 45.0.1 (#8826) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d867610c5b..498834ddb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ async_timeout==5.0.1; python_version <= "3.10" -cryptography==44.0.2 +cryptography==45.0.1 voluptuous==0.15.2 PyYAML==6.0.2 paho-mqtt==1.6.1 From e87b65948352a559457f96fb2cd0b6ac942f25f8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 17 May 2025 22:05:03 -0500 Subject: [PATCH 121/193] [sen5x] Fix validation for values read from hardware (#8769) --- esphome/components/sen5x/sen5x.cpp | 89 ++++++++++++++++++------------ 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 0efc961943..f29ba18d6e 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -25,6 +25,10 @@ static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2; static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; +static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // used for VOC and NOx index values +static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor +static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor + void SEN5XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up sen5x..."); @@ -88,8 +92,9 @@ void SEN5XComponent::setup() { product_name_.push_back(current_char); // second char current_char = *current_int & 0xFF; - if (current_char) + if (current_char) { product_name_.push_back(current_char); + } } current_int++; } while (current_char && --max); @@ -271,10 +276,10 @@ void SEN5XComponent::dump_config() { ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); break; case MEDIUM_ACCELERATION: - ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode"); + ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode"); break; case HIGH_ACCELERATION: - ESP_LOGCONFIG(TAG, " High RH/T accelertion mode"); + ESP_LOGCONFIG(TAG, " High RH/T acceleration mode"); break; } } @@ -337,47 +342,61 @@ void SEN5XComponent::update() { ESP_LOGD(TAG, "read data error (%d)", this->last_error_); return; } - float pm_1_0 = measurements[0] / 10.0; - if (measurements[0] == 0xFFFF) - pm_1_0 = NAN; - float pm_2_5 = measurements[1] / 10.0; - if (measurements[1] == 0xFFFF) - pm_2_5 = NAN; - float pm_4_0 = measurements[2] / 10.0; - if (measurements[2] == 0xFFFF) - pm_4_0 = NAN; - float pm_10_0 = measurements[3] / 10.0; - if (measurements[3] == 0xFFFF) - pm_10_0 = NAN; - float humidity = measurements[4] / 100.0; - if (measurements[4] == 0xFFFF) - humidity = NAN; - float temperature = (int16_t) measurements[5] / 200.0; - if (measurements[5] == 0xFFFF) - temperature = NAN; - float voc = measurements[6] / 10.0; - if (measurements[6] == 0xFFFF) - voc = NAN; - float nox = measurements[7] / 10.0; - if (measurements[7] == 0xFFFF) - nox = NAN; - if (this->pm_1_0_sensor_ != nullptr) + ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]); + float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f; + + ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]); + float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f; + + ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]); + float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f; + + ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]); + float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f; + + ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]); + float humidity = measurements[4] == INT16_MAX ? NAN : static_cast(measurements[4]) / 100.0f; + + ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]); + float temperature = measurements[5] == INT16_MAX ? NAN : static_cast(measurements[5]) / 200.0f; + + ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]); + int16_t voc_idx = static_cast(measurements[6]); + float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE) + ? NAN + : static_cast(voc_idx) / 10.0f; + + ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]); + int16_t nox_idx = static_cast(measurements[7]); + float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE) + ? NAN + : static_cast(nox_idx) / 10.0f; + + if (this->pm_1_0_sensor_ != nullptr) { this->pm_1_0_sensor_->publish_state(pm_1_0); - if (this->pm_2_5_sensor_ != nullptr) + } + if (this->pm_2_5_sensor_ != nullptr) { this->pm_2_5_sensor_->publish_state(pm_2_5); - if (this->pm_4_0_sensor_ != nullptr) + } + if (this->pm_4_0_sensor_ != nullptr) { this->pm_4_0_sensor_->publish_state(pm_4_0); - if (this->pm_10_0_sensor_ != nullptr) + } + if (this->pm_10_0_sensor_ != nullptr) { this->pm_10_0_sensor_->publish_state(pm_10_0); - if (this->temperature_sensor_ != nullptr) + } + if (this->temperature_sensor_ != nullptr) { this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) + } + if (this->humidity_sensor_ != nullptr) { this->humidity_sensor_->publish_state(humidity); - if (this->voc_sensor_ != nullptr) + } + if (this->voc_sensor_ != nullptr) { this->voc_sensor_->publish_state(voc); - if (this->nox_sensor_ != nullptr) + } + if (this->nox_sensor_ != nullptr) { this->nox_sensor_->publish_state(nox); + } this->status_clear_warning(); }); } From 18ac1b7c549ed4e3fe3e55dfdb81fc5bbb08a729 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Sun, 18 May 2025 09:11:09 +0600 Subject: [PATCH 122/193] Fix the case of single error (#8824) --- esphome/voluptuous_schema.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 15f9206f21..8fb966e3b2 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -15,7 +15,9 @@ class ExtraKeysInvalid(vol.Invalid): def ensure_multiple_invalid(err): if isinstance(err, vol.MultipleInvalid): return err - return vol.MultipleInvalid(err) + if isinstance(err, list): + return vol.MultipleInvalid(err) + return vol.MultipleInvalid([err]) # pylint: disable=protected-access, unidiomatic-typecheck From 44470f31f60d708abd74b262330173fdea1c048e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 18 May 2025 13:30:08 +1000 Subject: [PATCH 123/193] Revert "[binary_sensor] initial state refactor" (#8828) --- .../binary_sensor/binary_sensor.cpp | 16 ++++--- .../components/binary_sensor/binary_sensor.h | 2 +- esphome/components/binary_sensor/filter.cpp | 42 +++++++++---------- esphome/components/binary_sensor/filter.h | 20 ++++----- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 30fbe4f0b4..20604a0b7e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -15,17 +15,21 @@ void BinarySensor::publish_state(bool state) { if (!this->publish_dedup_.next(state)) return; if (this->filter_list_ == nullptr) { - this->send_state_internal(state); + this->send_state_internal(state, false); } else { - this->filter_list_->input(state); + this->filter_list_->input(state, false); } } void BinarySensor::publish_initial_state(bool state) { - this->has_state_ = false; - this->publish_state(state); + if (!this->publish_dedup_.next(state)) + return; + if (this->filter_list_ == nullptr) { + this->send_state_internal(state, true); + } else { + this->filter_list_->input(state, true); + } } -void BinarySensor::send_state_internal(bool state) { - bool is_initial = !this->has_state_; +void BinarySensor::send_state_internal(bool state, bool is_initial) { if (is_initial) { ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); } else { diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 9ba7aeeeff..57cae9e2f5 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool state); + void send_state_internal(bool state, bool is_initial); /// Return whether this binary sensor has outputted a state. virtual bool has_state() const; diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index fd6cc31008..8f94b108ac 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -9,37 +9,37 @@ namespace binary_sensor { static const char *const TAG = "sensor.filter"; -void Filter::output(bool value) { +void Filter::output(bool value, bool is_initial) { if (!this->dedup_.next(value)) return; if (this->next_ == nullptr) { - this->parent_->send_state_internal(value); + this->parent_->send_state_internal(value, is_initial); } else { - this->next_->input(value); + this->next_->input(value, is_initial); } } -void Filter::input(bool value) { - auto b = this->new_value(value); +void Filter::input(bool value, bool is_initial) { + auto b = this->new_value(value, is_initial); if (b.has_value()) { - this->output(*b); + this->output(*b, is_initial); } } -optional DelayedOnOffFilter::new_value(bool value) { +optional DelayedOnOffFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); + this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); } else { - this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); + this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); } return {}; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOnFilter::new_value(bool value) { +optional DelayedOnFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); + this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); return {}; } else { this->cancel_timeout("ON"); @@ -49,9 +49,9 @@ optional DelayedOnFilter::new_value(bool value) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOffFilter::new_value(bool value) { +optional DelayedOffFilter::new_value(bool value, bool is_initial) { if (!value) { - this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); + this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); return {}; } else { this->cancel_timeout("OFF"); @@ -61,11 +61,11 @@ optional DelayedOffFilter::new_value(bool value) { float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional InvertFilter::new_value(bool value) { return !value; } +optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} -optional AutorepeatFilter::new_value(bool value) { +optional AutorepeatFilter::new_value(bool value, bool is_initial) { if (value) { // Ignore if already running if (this->active_timing_ != 0) @@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() { void AutorepeatFilter::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val); + this->output(val, false); // This is at least the second one so not initial this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); } @@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} -optional LambdaFilter::new_value(bool value) { return this->f_(value); } +optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } -optional SettleFilter::new_value(bool value) { +optional SettleFilter::new_value(bool value, bool is_initial) { if (!this->steady_) { - this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { + this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { this->steady_ = true; - this->output(value); + this->output(value, is_initial); }); return {}; } else { this->steady_ = false; - this->output(value); + this->output(value, is_initial); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 65838da49d..f7342db2fb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -14,11 +14,11 @@ class BinarySensor; class Filter { public: - virtual optional new_value(bool value) = 0; + virtual optional new_value(bool value, bool is_initial) = 0; - void input(bool value); + void input(bool value, bool is_initial); - void output(bool value); + void output(bool value, bool is_initial); protected: friend BinarySensor; @@ -30,7 +30,7 @@ class Filter { class DelayedOnOffFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component { class InvertFilter : public Filter { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; }; struct AutorepeatFilterTiming { @@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component { public: explicit AutorepeatFilter(std::vector timings); - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -102,7 +102,7 @@ class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; protected: std::function(bool)> f_; @@ -110,7 +110,7 @@ class LambdaFilter : public Filter { class SettleFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; From a78bea78f9d7c6ea8f19acf7ae2999fee09075a9 Mon Sep 17 00:00:00 2001 From: Kent Gibson Date: Sun, 18 May 2025 11:45:12 +0800 Subject: [PATCH 124/193] Fix misspelling of climate in climate_ir.climate_ir_with_receiver_schema (#8829) --- esphome/components/ballu/climate.py | 2 +- esphome/components/climate_ir/__init__.py | 6 +++--- esphome/components/climate_ir_lg/climate.py | 2 +- esphome/components/coolix/climate.py | 2 +- esphome/components/daikin/climate.py | 2 +- esphome/components/daikin_arc/climate.py | 2 +- esphome/components/daikin_brc/climate.py | 2 +- esphome/components/delonghi/climate.py | 2 +- esphome/components/emmeti/climate.py | 2 +- esphome/components/fujitsu_general/climate.py | 2 +- esphome/components/gree/climate.py | 2 +- esphome/components/heatpumpir/climate.py | 2 +- esphome/components/hitachi_ac344/climate.py | 2 +- esphome/components/hitachi_ac424/climate.py | 2 +- esphome/components/midea_ir/climate.py | 2 +- esphome/components/mitsubishi/climate.py | 2 +- esphome/components/noblex/climate.py | 2 +- esphome/components/tcl112/climate.py | 2 +- esphome/components/toshiba/climate.py | 2 +- esphome/components/whirlpool/climate.py | 2 +- esphome/components/whynter/climate.py | 2 +- esphome/components/zhlt01/climate.py | 2 +- 22 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/ballu/climate.py b/esphome/components/ballu/climate.py index e35a1d244d..1127084632 100644 --- a/esphome/components/ballu/climate.py +++ b/esphome/components/ballu/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@bazuchan"] ballu_ns = cg.esphome_ns.namespace("ballu") BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate) async def to_code(config): diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 32b614e933..312b2ad900 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -40,7 +40,7 @@ def climate_ir_schema( ) -def climare_ir_with_receiver_schema( +def climate_ir_with_receiver_schema( class_: MockObjClass, ) -> cv.Schema: return climate_ir_schema(class_).extend( @@ -59,7 +59,7 @@ def deprecated_schema_constant(config): type = str(id.type).split("::", maxsplit=1)[0] _LOGGER.warning( "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " - "Please use `climate_ir.climare_ir_with_receiver_schema(...)` instead. " + "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. " "If you are seeing this, report an issue to the external_component author and ask them to update it. " "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " "Component using this schema: %s", @@ -68,7 +68,7 @@ def deprecated_schema_constant(config): return config -CLIMATE_IR_WITH_RECEIVER_SCHEMA = climare_ir_with_receiver_schema(ClimateIR) +CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR) CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py index de824bfe5c..9c832642ce 100644 --- a/esphome/components/climate_ir_lg/climate.py +++ b/esphome/components/climate_ir_lg/climate.py @@ -13,7 +13,7 @@ CONF_BIT_HIGH = "bit_high" CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ZERO_LOW = "bit_zero_low" -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(LgIrClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend( { cv.Optional( CONF_HEADER_HIGH, default="8000us" diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index b280544a5c..1ebcff3c1b 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"] coolix_ns = cg.esphome_ns.namespace("coolix") CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(CoolixClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate) async def to_code(config): diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py index 2cd44969c1..7f0226143b 100644 --- a/esphome/components/daikin/climate.py +++ b/esphome/components/daikin/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] daikin_ns = cg.esphome_ns.namespace("daikin") DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate) async def to_code(config): diff --git a/esphome/components/daikin_arc/climate.py b/esphome/components/daikin_arc/climate.py index 8f6b07315d..dbaf12d959 100644 --- a/esphome/components/daikin_arc/climate.py +++ b/esphome/components/daikin_arc/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinArcClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinArcClimate) async def to_code(config): diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py index 1000784380..5b7a4631a9 100644 --- a/esphome/components/daikin_brc/climate.py +++ b/esphome/components/daikin_brc/climate.py @@ -9,7 +9,7 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinBrcClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinBrcClimate).extend( { cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py index ff878b4ff7..63576f032d 100644 --- a/esphome/components/delonghi/climate.py +++ b/esphome/components/delonghi/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] delonghi_ns = cg.esphome_ns.namespace("delonghi") DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DelonghiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DelonghiClimate) async def to_code(config): diff --git a/esphome/components/emmeti/climate.py b/esphome/components/emmeti/climate.py index 042f1af64b..56e8e2b804 100644 --- a/esphome/components/emmeti/climate.py +++ b/esphome/components/emmeti/climate.py @@ -7,7 +7,7 @@ AUTO_LOAD = ["climate_ir"] emmeti_ns = cg.esphome_ns.namespace("emmeti") EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(EmmetiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(EmmetiClimate) async def to_code(config): diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py index 0f028d0af3..a104eafbcc 100644 --- a/esphome/components/fujitsu_general/climate.py +++ b/esphome/components/fujitsu_general/climate.py @@ -8,7 +8,7 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_( "FujitsuGeneralClimate", climate_ir.ClimateIR ) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(FujitsuGeneralClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FujitsuGeneralClimate) async def to_code(config): diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py index 947ef9bb97..057ba67b94 100644 --- a/esphome/components/gree/climate.py +++ b/esphome/components/gree/climate.py @@ -21,7 +21,7 @@ MODELS = { "yag": Model.GREE_YAG, } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(GreeClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend( { cv.Required(CONF_MODEL): cv.enum(MODELS), } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 21b0168615..c0eb8db4b3 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -97,7 +97,7 @@ VERTICAL_DIRECTIONS = { } CONFIG_SCHEMA = cv.All( - climate_ir.climare_ir_with_receiver_schema(HeatpumpIRClimate).extend( + climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend( { cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py index 4fa2d54fbb..ebdf4e8db4 100644 --- a/esphome/components/hitachi_ac344/climate.py +++ b/esphome/components/hitachi_ac344/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate) async def to_code(config): diff --git a/esphome/components/hitachi_ac424/climate.py b/esphome/components/hitachi_ac424/climate.py index 4b20147922..fde4e77545 100644 --- a/esphome/components/hitachi_ac424/climate.py +++ b/esphome/components/hitachi_ac424/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate) async def to_code(config): diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 5c9256b5e4..cbf5fae6fe 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -10,7 +10,7 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir") MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MideaIR).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend( { cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py index 5784d3ee8a..8291d70346 100644 --- a/esphome/components/mitsubishi/climate.py +++ b/esphome/components/mitsubishi/climate.py @@ -43,7 +43,7 @@ VERTICAL_DIRECTIONS = { } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MitsubishiClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend( { cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE), cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean, diff --git a/esphome/components/noblex/climate.py b/esphome/components/noblex/climate.py index d619265d01..19c4b6a08e 100644 --- a/esphome/components/noblex/climate.py +++ b/esphome/components/noblex/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] noblex_ns = cg.esphome_ns.namespace("noblex") NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(NoblexClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(NoblexClimate) async def to_code(config): diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 9864113a52..58ed7ee529 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"] tcl112_ns = cg.esphome_ns.namespace("tcl112") Tcl112Climate = tcl112_ns.class_("Tcl112Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Tcl112Climate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(Tcl112Climate) async def to_code(config): diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 40112fc460..b8e390dd66 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -16,7 +16,7 @@ MODELS = { "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ToshibaClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend( { cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True), } diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py index daee9e7fb7..f969a505fb 100644 --- a/esphome/components/whirlpool/climate.py +++ b/esphome/components/whirlpool/climate.py @@ -15,7 +15,7 @@ MODELS = { "DG11J1-91": Model.MODEL_DG11J1_91, } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(WhirlpoolClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend( { cv.Optional(CONF_MODEL, default="DG11J1-3A"): cv.enum(MODELS, upper=True), } diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py index 4a01c014c7..bf33890d9c 100644 --- a/esphome/components/whynter/climate.py +++ b/esphome/components/whynter/climate.py @@ -9,7 +9,7 @@ whynter_ns = cg.esphome_ns.namespace("whynter") Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Whynter).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(Whynter).extend( { cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } diff --git a/esphome/components/zhlt01/climate.py b/esphome/components/zhlt01/climate.py index d5098ab42c..8d0c50308b 100644 --- a/esphome/components/zhlt01/climate.py +++ b/esphome/components/zhlt01/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@cfeenstra1024"] zhlt01_ns = cg.esphome_ns.namespace("zhlt01") ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ZHLT01Climate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ZHLT01Climate) async def to_code(config): From e47741d47174bca80b531a2b6ffd6017951cfcb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 15:43:41 -0400 Subject: [PATCH 125/193] Fix ESP32 console logging corruption and message loss in multi-task (#8806) --- esphome/components/logger/logger.cpp | 44 +++++++++++++++++++--------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 0ad909cb07..014f7e3dec 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -16,9 +16,14 @@ static const char *const TAG = "logger"; #ifdef USE_ESP32 // Implementation for ESP32 (multi-task platform with task-specific tracking) // Main task always uses direct buffer access for console output and callbacks -// Other tasks: -// - With task log buffer: stack buffer for console output, async buffer for callbacks -// - Without task log buffer: only console output, no callbacks +// +// For non-main tasks: +// - WITH task log buffer: Prefer sending to ring buffer for async processing +// - Avoids allocating stack memory for console output in normal operation +// - Prevents console corruption from concurrent writes by multiple tasks +// - Messages are serialized through main loop for proper console output +// - Fallback to emergency console logging only if ring buffer is full +// - WITHOUT task log buffer: Only emergency console output, no callbacks void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag)) return; @@ -38,8 +43,18 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * return; } - // For non-main tasks: use stack-allocated buffer only for console output - if (this->baud_rate_ > 0) { // If logging is enabled, write to console + bool message_sent = false; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered + message_sent = this->log_buffer_->send_message_thread_safe(static_cast(level), tag, + static_cast(line), current_task, format, args); +#endif // USE_ESPHOME_TASK_LOG_BUFFER + + // Emergency console logging for non-main tasks when ring buffer is full or disabled + // This is a fallback mechanism to ensure critical log messages are visible + // Note: This may cause interleaved/corrupted console output if multiple tasks + // log simultaneously, but it's better than losing important messages entirely + if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety @@ -49,15 +64,6 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * this->write_msg_(console_buffer); } -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered - if (this->log_callback_.size() > 0) { - // This will be processed in the main loop - this->log_buffer_->send_message_thread_safe(static_cast(level), tag, static_cast(line), - current_task, format, args); - } -#endif // USE_ESPHOME_TASK_LOG_BUFFER - // Reset the recursion guard for this task this->reset_task_log_recursion_(is_main_task); } @@ -184,7 +190,17 @@ void Logger::loop() { this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); + // At this point all the data we need from message has been transferred to the tx_buffer + // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); + + // Write to console from the main loop to prevent corruption from concurrent writes + // This ensures all log messages appear on the console in a clean, serialized manner + // Note: Messages may appear slightly out of order due to async processing, but + // this is preferred over corrupted/interleaved console output + if (this->baud_rate_ > 0) { + this->write_msg_(this->tx_buffer_); + } } } #endif From 574aabdedec80ca20dec5a54439a9ad649bf8cb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 15:48:57 -0400 Subject: [PATCH 126/193] Reduce number of calls to fetch time in the main loop (#8804) --- esphome/components/api/api_connection.cpp | 5 +-- esphome/components/bedjet/bedjet_hub.cpp | 1 + .../bluetooth_proxy/bluetooth_proxy.cpp | 3 +- esphome/components/cse7766/cse7766.cpp | 3 +- .../current_based/current_based_cover.cpp | 3 +- esphome/components/daly_bms/daly_bms.cpp | 3 +- esphome/components/debug/debug_component.cpp | 2 +- esphome/components/endstop/endstop_cover.cpp | 3 +- .../components/esp32_ble/ble_advertising.cpp | 3 +- .../components/esp32_camera/esp32_camera.cpp | 3 +- .../esp32_improv/esp32_improv_component.cpp | 2 +- .../components/esp32_touch/esp32_touch.cpp | 2 +- .../ethernet/ethernet_component.cpp | 2 +- .../components/feedback/feedback_cover.cpp | 3 +- esphome/components/gcja5/gcja5.cpp | 3 +- .../growatt_solar/growatt_solar.cpp | 3 +- esphome/components/kuntze/kuntze.cpp | 3 +- .../matrix_keypad/matrix_keypad.cpp | 3 +- .../components/max7219digit/max7219digit.cpp | 3 +- esphome/components/modbus/modbus.cpp | 3 +- esphome/components/mqtt/mqtt_client.cpp | 2 +- esphome/components/pmsx003/pmsx003.cpp | 3 +- esphome/components/pzem004t/pzem004t.cpp | 3 +- esphome/components/rf_bridge/rf_bridge.cpp | 3 +- esphome/components/sds011/sds011.cpp | 3 +- .../components/slow_pwm/slow_pwm_output.cpp | 3 +- esphome/components/sprinkler/sprinkler.cpp | 11 ++++--- .../time_based/time_based_cover.cpp | 3 +- .../components/uart/switch/uart_switch.cpp | 3 +- .../climate/uponor_smatrix_climate.cpp | 3 +- .../uponor_smatrix/uponor_smatrix.cpp | 3 +- esphome/core/application.cpp | 32 +++++++++++++------ esphome/core/application.h | 6 +++- esphome/core/component.cpp | 15 ++++++--- esphome/core/component.h | 6 +++- esphome/core/scheduler.cpp | 5 ++- 36 files changed, 107 insertions(+), 53 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c377909951..1c945b200f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -8,6 +8,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/version.h" +#include "esphome/core/application.h" #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -146,7 +147,7 @@ void APIConnection::loop() { } return; } else { - this->last_traffic_ = millis(); + this->last_traffic_ = App.get_loop_component_start_time(); // read a packet this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); if (this->remove_) @@ -165,7 +166,7 @@ void APIConnection::loop() { static uint32_t keepalive = 60000; static uint8_t max_ping_retries = 60; static uint16_t ping_retry_interval = 1000; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (keepalive * 5) / 2) { diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 6404298697..fea7080de6 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -3,6 +3,7 @@ #include "bedjet_hub.h" #include "bedjet_child.h" #include "bedjet_const.h" +#include "esphome/core/application.h" #include namespace esphome { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 915d2882d3..d8b2111cb0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" +#include "esphome/core/application.h" #ifdef USE_ESP32 @@ -177,7 +178,7 @@ void BluetoothProxy::loop() { // Flush any pending BLE advertisements that have been accumulated but not yet sent if (this->raw_advertisements_) { static uint32_t last_flush_time = 0; - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); // Flush accumulated advertisements every 100ms if (now - last_flush_time >= 100) { diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 88a91e374a..b0876778a3 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,5 +1,6 @@ #include "cse7766.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace cse7766 { @@ -7,7 +8,7 @@ namespace cse7766 { static const char *const TAG = "cse7766"; void CSE7766Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_transmission_ >= 500) { // last transmission too long ago. Reset RX index. this->raw_data_index_ = 0; diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 8404e07894..8bb27dbeca 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -1,6 +1,7 @@ #include "current_based_cover.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include namespace esphome { @@ -60,7 +61,7 @@ void CurrentBasedCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->current_operation == COVER_OPERATION_OPENING) { if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 929f31e008..1dd0520465 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -1,6 +1,7 @@ #include "daly_bms.h" #include #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace daly_bms { @@ -32,7 +33,7 @@ void DalyBmsComponent::update() { } void DalyBmsComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->receiving_ && (now - this->last_transmission_ >= 200)) { // last transmission too long ago. Reset RX index. ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index."); diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 5bcc676247..c4de42c7e9 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -70,7 +70,7 @@ void DebugComponent::loop() { #ifdef USE_SENSOR // calculate loop time - from last call to this one if (this->loop_time_sensor_ != nullptr) { - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); uint32_t loop_time = now - this->last_loop_timetag_; this->max_loop_time_ = std::max(this->max_loop_time_, loop_time); this->last_loop_timetag_ = now; diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 1190acc46b..381f098eb5 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -1,6 +1,7 @@ #include "endstop_cover.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" namespace esphome { namespace endstop { @@ -65,7 +66,7 @@ void EndstopCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) { float dur = (now - this->start_dir_time_) / 1e3f; diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 1d340c76d9..8d43b5af33 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -6,6 +6,7 @@ #include #include "ble_uuid.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace esp32_ble { @@ -143,7 +144,7 @@ void BLEAdvertising::loop() { if (this->raw_advertisements_callbacks_.empty()) { return; } - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { this->stop(); this->current_adv_index_ += 1; diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index e9e9d3cffb..60accba747 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -3,6 +3,7 @@ #include "esp32_camera.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include @@ -162,7 +163,7 @@ void ESP32Camera::loop() { } // request idle image every idle_update_interval - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { this->last_idle_request_ = now; this->request_image(IDLE); diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index b720425506..d74714838f 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -92,7 +92,7 @@ void ESP32ImprovComponent::loop() { if (!this->incoming_data_.empty()) this->process_incoming_data_(); - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case improv::STATE_STOPPED: diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 69e4e37cec..1dcb39e5de 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -288,7 +288,7 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { } void ESP32TouchComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { child->value_ = this->component_touch_pad_read(child->get_touch_pad()); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 68a6e617fc..79ef0715d8 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -240,7 +240,7 @@ void EthernetComponent::setup() { } void EthernetComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case EthernetComponentState::STOPPED: diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index fa3166ba65..e419ee6229 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -1,6 +1,7 @@ #include "feedback_cover.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace feedback { @@ -220,7 +221,7 @@ void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_o void FeedbackCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Recompute position every loop cycle this->recompute_position_(); diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index b1db58654b..64b237515b 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -6,6 +6,7 @@ */ #include "gcja5.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include namespace esphome { @@ -16,7 +17,7 @@ static const char *const TAG = "gcja5"; void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } void GCJA5Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_transmission_ >= 500) { // last transmission too long ago. Reset RX index. this->rx_message_.clear(); diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index c4ed5ab841..60fd1379e8 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -1,5 +1,6 @@ #include "growatt_solar.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace growatt_solar { @@ -18,7 +19,7 @@ void GrowattSolar::loop() { void GrowattSolar::update() { // If our last send has had no reply yet, and it wasn't that long ago, do nothing. - uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_send_ < this->get_update_interval() / 2) { return; } diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index e50dafca86..8ab7af8cd9 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -1,5 +1,6 @@ #include "kuntze.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace kuntze { @@ -60,7 +61,7 @@ void Kuntze::on_modbus_data(const std::vector &data) { } void Kuntze::loop() { - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); // timeout after 15 seconds if (this->waiting_ && (now - this->last_send_ > 15000)) { ESP_LOGW(TAG, "timed out waiting for response"); diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 8537997935..6cb4fc4f3c 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -1,5 +1,6 @@ #include "matrix_keypad.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace matrix_keypad { @@ -28,7 +29,7 @@ void MatrixKeypad::setup() { void MatrixKeypad::loop() { static uint32_t active_start = 0; static int active_key = -1; - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); int key = -1; bool error = false; int pos = 0, row, col; diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 13b75ca734..154accd66f 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "max7219font.h" #include @@ -63,7 +64,7 @@ void MAX7219Component::dump_config() { } void MAX7219Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); const uint32_t millis_since_last_scroll = now - this->last_scroll_; const size_t first_line_size = this->max_displaybuffer_[0].size(); // check if the buffer has shrunk past the current position since last update diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 47deea83e6..80c2ffe3d6 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -1,6 +1,7 @@ #include "modbus.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" namespace esphome { namespace modbus { @@ -13,7 +14,7 @@ void Modbus::setup() { } } void Modbus::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); while (this->available()) { uint8_t byte; diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1fcef3293c..e3722099a7 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -345,7 +345,7 @@ void MQTTClientComponent::loop() { this->disconnect_reason_.reset(); } - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case MQTT_CLIENT_DISABLED: diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 11626768d8..0abed8a5a4 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -1,5 +1,6 @@ #include "pmsx003.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace pmsx003 { @@ -42,7 +43,7 @@ void PMSX003Component::dump_config() { } void PMSX003Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // If we update less often than it takes the device to stabilise, spin the fan down // rather than running it constantly. It does take some time to stabilise, so we diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index 35b66b03f2..356847825e 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -1,5 +1,6 @@ #include "pzem004t.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include namespace esphome { @@ -16,7 +17,7 @@ void PZEM004T::setup() { } void PZEM004T::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_read_ > 500 && this->available() < 7) { while (this->available()) this->read(); diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 3b3e00a416..52ce037dbe 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -1,5 +1,6 @@ #include "rf_bridge.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include #include @@ -128,7 +129,7 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) { } void RFBridgeComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_bridge_byte_ > 50) { this->rx_buffer_.clear(); this->last_bridge_byte_ = now; diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 0c04ff557f..a34059d85d 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -1,5 +1,6 @@ #include "sds011.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace sds011 { @@ -75,7 +76,7 @@ void SDS011Component::dump_config() { } void SDS011Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if ((now - this->last_transmission_ >= 500) && this->data_index_) { // last transmission too long ago. Reset RX index. ESP_LOGV(TAG, "Last transmission too long ago. Reset RX index."); diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index d6b2cdfe12..643294303c 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -1,5 +1,6 @@ #include "slow_pwm_output.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace slow_pwm { @@ -39,7 +40,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) { } void SlowPWMOutput::loop() { - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); float scaled_state = this->state_ * this->period_; if (now - this->period_start_time_ >= this->period_) { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 3cfb5ccdee..50ea3eff51 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -20,7 +20,7 @@ SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *o bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } void SprinklerSwitch::loop() { - if ((this->pinned_millis_) && (millis() > this->pinned_millis_ + this->pulse_duration_)) { + if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) { this->pinned_millis_ = 0; // reset tracker if (this->off_switch_->state) { this->off_switch_->turn_off(); @@ -148,22 +148,23 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler : controller_(controller), valve_(valve) {} void SprinklerValveOperator::loop() { - if (millis() >= this->start_millis_) { // dummy check + uint32_t now = App.get_loop_component_start_time(); + if (now >= this->start_millis_) { // dummy check switch (this->state_) { case STARTING: - if (millis() > (this->start_millis_ + this->start_delay_)) { + if (now > (this->start_millis_ + this->start_delay_)) { this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state } break; case ACTIVE: - if (millis() > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { + if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down } break; case STOPPING: - if (millis() > (this->stop_millis_ + this->stop_delay_)) { + if (now > (this->stop_millis_ + this->stop_delay_)) { this->kill_(); // stop_delay_has been exceeded, ensure all valves are off } break; diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index e1936d5ee1..ec219d6db7 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -1,6 +1,7 @@ #include "time_based_cover.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" namespace esphome { namespace time_based { @@ -26,7 +27,7 @@ void TimeBasedCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Recompute position every loop cycle this->recompute_position_(); diff --git a/esphome/components/uart/switch/uart_switch.cpp b/esphome/components/uart/switch/uart_switch.cpp index 1edb54641b..96f50ff50f 100644 --- a/esphome/components/uart/switch/uart_switch.cpp +++ b/esphome/components/uart/switch/uart_switch.cpp @@ -1,5 +1,6 @@ #include "uart_switch.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace uart { @@ -8,7 +9,7 @@ static const char *const TAG = "uart.switch"; void UARTSwitch::loop() { if (this->send_every_) { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_transmission_ > this->send_every_) { this->write_command_(this->state); this->last_transmission_ = now; diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 5afc628db3..cc9b8a0f90 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -1,6 +1,7 @@ #include "uponor_smatrix_climate.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace uponor_smatrix { @@ -13,7 +14,7 @@ void UponorSmatrixClimate::dump_config() { } void UponorSmatrixClimate::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Publish state after all update packets are processed if (this->last_data_ != 0 && (now - this->last_data_ > 100) && this->target_temperature_raw_ != 0) { diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index e058de2852..2dbbef72ab 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -1,5 +1,6 @@ #include "uponor_smatrix.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace uponor_smatrix { @@ -35,7 +36,7 @@ void UponorSmatrixComponent::dump_config() { } void UponorSmatrixComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Discard stale data if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) { diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3f5a283fd8..a81a2c580c 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -67,22 +67,32 @@ void Application::loop() { uint32_t new_app_state = 0; this->scheduler.call(); - this->feed_wdt(); + + // Get the initial loop time at the start + uint32_t last_op_end_time = millis(); + + // Feed WDT with time + this->feed_wdt(last_op_end_time); + for (Component *component : this->looping_components_) { + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + { this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component}; + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; component->call(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); } new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; - this->feed_wdt(); + this->feed_wdt(last_op_end_time); } this->app_state_ = new_app_state; - const uint32_t now = millis(); - - auto elapsed = now - this->last_loop_; + // Use the last component's end time instead of calling millis() again + auto elapsed = last_op_end_time - this->last_loop_; if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { yield(); } else { @@ -94,7 +104,7 @@ void Application::loop() { delay_time = std::min(next_schedule, delay_time); delay(delay_time); } - this->last_loop_ = now; + this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ == 0) { @@ -109,10 +119,12 @@ void Application::loop() { } } -void IRAM_ATTR HOT Application::feed_wdt() { +void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { static uint32_t last_feed = 0; - uint32_t now = micros(); - if (now - last_feed > 3000) { + // Use provided time if available, otherwise get current time + uint32_t now = time ? time : millis(); + // Compare in milliseconds (3ms threshold) + if (now - last_feed > 3) { arch_feed_wdt(); last_feed = now; #ifdef USE_STATUS_LED diff --git a/esphome/core/application.h b/esphome/core/application.h index e64e2b7655..aa44d9ba1d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -217,6 +217,9 @@ class Application { std::string get_compilation_time() const { return this->compilation_time_; } + /// Get the cached time in milliseconds from when the current component started its loop execution + inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; } + /** Set the target interval with which to run the loop() calls. * If the loop() method takes longer than the target interval, ESPHome won't * sleep in loop(), but if the time spent in loop() is small than the target, ESPHome @@ -236,7 +239,7 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } - void feed_wdt(); + void feed_wdt(uint32_t time = 0); void reboot(); @@ -551,6 +554,7 @@ class Application { size_t dump_config_at_{SIZE_MAX}; uint32_t app_state_{0}; Component *current_component_{nullptr}; + uint32_t loop_component_start_time_{0}; }; /// Global storage of Application pointer - only one Application can exist. diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a7e451b93d..1141e4067d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -240,10 +240,12 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } -WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) - : started_(millis()), component_(component) {} -WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { - uint32_t blocking_time = millis() - this->started_; +WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) + : started_(start_time), component_(component) {} +uint32_t WarnIfComponentBlockingGuard::finish() { + uint32_t curr_time = millis(); + + uint32_t blocking_time = curr_time - this->started_; bool should_warn; if (this->component_ != nullptr) { should_warn = this->component_->should_warn_of_blocking(blocking_time); @@ -254,8 +256,11 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { const char *src = component_ == nullptr ? "" : component_->get_component_source(); ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms."); - ; } + + return curr_time; } +WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 412074282d..7b3e12eb59 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -339,7 +339,11 @@ class PollingComponent : public Component { class WarnIfComponentBlockingGuard { public: - WarnIfComponentBlockingGuard(Component *component); + WarnIfComponentBlockingGuard(Component *component, uint32_t start_time); + + // Finish the timing operation and return the current time + uint32_t finish(); + ~WarnIfComponentBlockingGuard(); protected: diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b4f617d405..2dea450ead 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -229,8 +229,11 @@ void HOT Scheduler::call() { // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled { - WarnIfComponentBlockingGuard guard{item->component}; + uint32_t now_ms = millis(); + WarnIfComponentBlockingGuard guard{item->component, now_ms}; item->callback(); + // Call finish to ensure blocking time is properly calculated and reported + guard.finish(); } } From b0c1e0e28ce8633de259a02bd9a1c9ec7b87e1c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 17:05:20 -0400 Subject: [PATCH 127/193] Refactor API frame helpers to enable buffer reuse (#8825) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 9 +- esphome/components/api/api_frame_helper.cpp | 128 ++++++++++++++------ esphome/components/api/api_frame_helper.h | 41 ++++++- esphome/components/api/proto.h | 28 +++++ 5 files changed, 162 insertions(+), 46 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1c945b200f..b7db88e6ba 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1645,7 +1645,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) return false; } - APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); + APIError err = this->helper_->write_protobuf_packet(message_type, buffer); if (err == APIError::WOULD_BLOCK) return false; if (err != APIError::OK) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index dd24802e72..e8a29b9c7f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -405,7 +405,14 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { // FIXME: ensure no recursive writes can happen this->proto_write_buffer_.clear(); - this->proto_write_buffer_.reserve(reserve_size); + // Get header padding size - used for both reserve and insert + uint8_t header_padding = this->helper_->frame_header_padding(); + // Reserve space for header padding + message + footer + // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) + // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) + this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); + // Insert header padding bytes so message encoding starts at the correct position + this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0); return {&this->proto_write_buffer_}; } bool try_to_clear_buffer(bool log_out_of_space); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index f251ceb6e4..f18f4104b6 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -493,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea std::vector data; data.resize(reason.length() + 1); data[0] = 0x01; // failure - for (size_t i = 0; i < reason.length(); i++) { - data[i + 1] = (uint8_t) reason[i]; + + // Copy error message in bulk + if (!reason.empty()) { + std::memcpy(data.data() + 1, reason.c_str(), reason.length()); } + // temporarily remove failed state auto orig_state = state_; state_ = State::EXPLICIT_REJECT; @@ -557,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } -APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { +APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { int err; APIError aerr; aerr = state_action_(); @@ -569,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload return APIError::WOULD_BLOCK; } + std::vector *raw_buffer = buffer.get_buffer(); + // Message data starts after padding + size_t payload_len = raw_buffer->size() - frame_header_padding_; size_t padding = 0; size_t msg_len = 4 + payload_len + padding; - size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); - auto tmpbuf = std::unique_ptr{new (std::nothrow) uint8_t[frame_len]}; - if (tmpbuf == nullptr) { - HELPER_LOG("Could not allocate for writing packet"); - return APIError::OUT_OF_MEMORY; - } - tmpbuf[0] = 0x01; // indicator - // tmpbuf[1], tmpbuf[2] to be set later + // We need to resize to include MAC space, but we already reserved it in create_buffer + raw_buffer->resize(raw_buffer->size() + frame_footer_size_); + + // Write the noise header in the padded area + // Buffer layout: + // [0] - 0x01 indicator byte + // [1-2] - Size of encrypted payload (filled after encryption) + // [3-4] - Message type (encrypted) + // [5-6] - Payload length (encrypted) + // [7...] - Actual payload data (encrypted) + uint8_t *buf_start = raw_buffer->data(); + buf_start[0] = 0x01; // indicator + // buf_start[1], buf_start[2] to be set later after encryption const uint8_t msg_offset = 3; - const uint8_t payload_offset = msg_offset + 4; - tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type - tmpbuf[msg_offset + 1] = (uint8_t) type; - tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len - tmpbuf[msg_offset + 3] = (uint8_t) payload_len; - // copy data - std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); - // fill padding with zeros - std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); + buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte + buf_start[msg_offset + 1] = (uint8_t) type; // type low byte + buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte + buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte + // payload data is already in the buffer starting at position 7 NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); + // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption + noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_); err = noise_cipherstate_encrypt(send_cipher_, &mbuf); if (err != 0) { state_ = State::FAILED; @@ -602,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload } size_t total_len = 3 + mbuf.size; - tmpbuf[1] = (uint8_t) (mbuf.size >> 8); - tmpbuf[2] = (uint8_t) mbuf.size; + buf_start[1] = (uint8_t) (mbuf.size >> 8); + buf_start[2] = (uint8_t) mbuf.size; struct iovec iov; - iov.iov_base = &tmpbuf[0]; + // Point iov_base to the beginning of the buffer (no unused padding in Noise) + // We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC) + iov.iov_base = buf_start; iov.iov_len = total_len; // write raw to not have two packets sent if NAGLE disabled @@ -718,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { return APIError::HANDSHAKESTATE_SPLIT_FAILED; } + frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); + HELPER_LOG("Handshake complete!"); noise_handshakestate_free(handshake_); handshake_ = nullptr; @@ -990,28 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } -APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { +APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { if (state_ != State::DATA) { return APIError::BAD_STATE; } - std::vector header; - header.reserve(1 + api::ProtoSize::varint(static_cast(payload_len)) + - api::ProtoSize::varint(static_cast(type))); - header.push_back(0x00); - ProtoVarInt(payload_len).encode(header); - ProtoVarInt(type).encode(header); + std::vector *raw_buffer = buffer.get_buffer(); + // Message data starts after padding (frame_header_padding_ = 6) + size_t payload_len = raw_buffer->size() - frame_header_padding_; - struct iovec iov[2]; - iov[0].iov_base = &header[0]; - iov[0].iov_len = header.size(); - if (payload_len == 0) { - return write_raw_(iov, 1); + // Calculate varint sizes for header components + size_t size_varint_len = api::ProtoSize::varint(static_cast(payload_len)); + size_t type_varint_len = api::ProtoSize::varint(static_cast(type)); + size_t total_header_len = 1 + size_varint_len + type_varint_len; + + if (total_header_len > frame_header_padding_) { + // Header is too large to fit in the padding + return APIError::BAD_ARG; } - iov[1].iov_base = const_cast(payload); - iov[1].iov_len = payload_len; - return write_raw_(iov, 2); + // Calculate where to start writing the header + // The header starts at the latest possible position to minimize unused padding + // + // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 + // [0-2] - Unused padding + // [3] - 0x00 indicator byte + // [4] - Payload size varint (1 byte, for sizes 0-127) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 + // [0-1] - Unused padding + // [2] - 0x00 indicator byte + // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 + // [0] - 0x00 indicator byte + // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) + // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [6...] - Actual payload data + uint8_t *buf_start = raw_buffer->data(); + size_t header_offset = frame_header_padding_ - total_header_len; + + // Write the plaintext header + buf_start[header_offset] = 0x00; // indicator + + // Encode size varint directly into buffer + ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + + // Encode type varint directly into buffer + ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + + struct iovec iov; + // Point iov_base to the beginning of our header (skip unused padding) + // This ensures we only send the actual header and payload, not the empty padding bytes + iov.iov_base = buf_start + header_offset; + iov.iov_len = total_header_len + payload_len; + + return write_raw_(&iov, 1); } APIError APIPlaintextFrameHelper::try_send_tx_buf_() { // try send from tx_buf diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index db506ea1ce..25bfd594ec 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -16,6 +16,8 @@ namespace esphome { namespace api { +class ProtoWriteBuffer; + struct ReadPacketBuffer { std::vector container; uint16_t type; @@ -65,32 +67,46 @@ class APIFrameHelper { virtual APIError loop() = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual bool can_write_without_blocking() = 0; - virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; + virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0; virtual std::string getpeername() = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; virtual APIError close() = 0; virtual APIError shutdown(int how) = 0; // Give this helper a name for logging virtual void set_log_info(std::string info) = 0; + // Get the frame header padding required by this protocol + virtual uint8_t frame_header_padding() = 0; + // Get the frame footer size required by this protocol + virtual uint8_t frame_footer_size() = 0; protected: // Common implementation for writing raw data to socket template APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); + + uint8_t frame_header_padding_{0}; + uint8_t frame_footer_size_{0}; }; #ifdef USE_API_NOISE class APINoiseFrameHelper : public APIFrameHelper { public: APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) - : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {} + : socket_(std::move(socket)), ctx_(std::move(ctx)) { + // Noise header structure: + // Pos 0: indicator (0x01) + // Pos 1-2: encrypted payload size (16-bit big-endian) + // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) + // Pos 7+: actual payload data + frame_header_padding_ = 7; + } ~APINoiseFrameHelper() override; APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; bool can_write_without_blocking() override; - APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; std::string getpeername() override { return this->socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return this->socket_->getpeername(addr, addrlen); @@ -99,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError shutdown(int how) override; // Give this helper a name for logging void set_log_info(std::string info) override { info_ = std::move(info); } + // Get the frame header padding required by this protocol + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } protected: struct ParsedFrame { @@ -152,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper { #ifdef USE_API_PLAINTEXT class APIPlaintextFrameHelper : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) { + // Plaintext header structure (worst case): + // Pos 0: indicator (0x00) + // Pos 1-3: payload size varint (up to 3 bytes) + // Pos 4-5: message type varint (up to 2 bytes) + // Pos 6+: actual payload data + frame_header_padding_ = 6; + } ~APIPlaintextFrameHelper() override = default; APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; bool can_write_without_blocking() override; - APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; std::string getpeername() override { return this->socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return this->socket_->getpeername(addr, addrlen); @@ -167,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError shutdown(int how) override; // Give this helper a name for logging void set_log_info(std::string info) override { info_ = std::move(info); } + // Get the frame header padding required by this protocol + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } protected: struct ParsedFrame { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e110a58eda..65bef0b6f7 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -83,6 +83,34 @@ class ProtoVarInt { return static_cast(this->value_ >> 1); } } + /** + * Encode the varint value to a pre-allocated buffer without bounds checking. + * + * @param buffer The pre-allocated buffer to write the encoded varint to + * @param len The size of the buffer in bytes + * + * @note The caller is responsible for ensuring the buffer is large enough + * to hold the encoded value. Use ProtoSize::varint() to calculate + * the exact size needed before calling this method. + * @note No bounds checking is performed for performance reasons. + */ + void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) { + uint64_t val = this->value_; + if (val <= 0x7F) { + buffer[0] = val; + return; + } + size_t i = 0; + while (val && i < len) { + uint8_t temp = val & 0x7F; + val >>= 7; + if (val) { + buffer[i++] = temp | 0x80; + } else { + buffer[i++] = temp; + } + } + } void encode(std::vector &out) { uint64_t val = this->value_; if (val <= 0x7F) { From 24fbe602dd4c888a2db53ead21d42f1b2878321e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 09:13:20 +1200 Subject: [PATCH 128/193] Bump codecov/codecov-action from 5.4.2 to 5.4.3 (#8820) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d2ec68010..4913e8ee19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} From 660030d1573cb281698fec95c55f8f471613c382 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 09:20:29 +1200 Subject: [PATCH 129/193] Bump docker/build-push-action from 6.16.0 to 6.17.0 in /.github/actions/build-image (#8810) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 3d6de54f42..0e41314ce6 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.17.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.17.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 9704de66478676340d0c7ad4b9a24edd7be20b81 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 15 May 2025 14:40:11 +1200 Subject: [PATCH 130/193] Update some sensor schemas to be Optional (#8803) --- esphome/components/ccs811/sensor.py | 15 ++++++----- esphome/components/dps310/sensor.py | 12 ++++----- esphome/components/ee895/sensor.py | 18 ++++++------- esphome/components/ens160_base/__init__.py | 21 ++++++++------- esphome/components/hte501/sensor.py | 12 ++++----- esphome/components/hyt271/sensor.py | 12 ++++----- esphome/components/mhz19/sensor.py | 18 ++++++++----- esphome/components/ms5611/sensor.py | 12 ++++----- esphome/components/ms8607/sensor.py | 6 ++--- esphome/components/senseair/sensor.py | 6 ++--- esphome/components/sgp30/sensor.py | 30 ++++++++++------------ esphome/components/shtcx/sensor.py | 12 ++++----- esphome/components/t6615/sensor.py | 6 ++--- esphome/components/t6615/t6615.cpp | 3 ++- 14 files changed, 96 insertions(+), 87 deletions(-) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 10565cb328..d9023a415f 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -32,14 +32,14 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(CCS811Component), - cv.Required(CONF_ECO2): sensor.sensor_schema( + cv.Optional(CONF_ECO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_TVOC): sensor.sensor_schema( + cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, @@ -64,10 +64,13 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - sens = await sensor.new_sensor(config[CONF_ECO2]) - cg.add(var.set_co2(sens)) - sens = await sensor.new_sensor(config[CONF_TVOC]) - cg.add(var.set_tvoc(sens)) + if eco2_config := config.get(CONF_ECO2): + sens = await sensor.new_sensor(eco2_config) + cg.add(var.set_co2(sens)) + + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) + cg.add(var.set_tvoc(sens)) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/dps310/sensor.py b/esphome/components/dps310/sensor.py index b2a98e5bab..605812beaa 100644 --- a/esphome/components/dps310/sensor.py +++ b/esphome/components/dps310/sensor.py @@ -27,14 +27,14 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(DPS310Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, icon=ICON_GAUGE, accuracy_decimals=1, @@ -53,10 +53,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py index 0f8f44c8a2..8c9c7e7238 100644 --- a/esphome/components/ee895/sensor.py +++ b/esphome/components/ee895/sensor.py @@ -26,19 +26,19 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(EE895Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, accuracy_decimals=1, device_class=DEVICE_CLASS_PRESSURE, @@ -56,14 +56,14 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py index 28e77e322b..3b6ad8a4ee 100644 --- a/esphome/components/ens160_base/__init__.py +++ b/esphome/components/ens160_base/__init__.py @@ -28,21 +28,21 @@ UNIT_INDEX = "index" CONFIG_SCHEMA_BASE = cv.Schema( { - cv.Required(CONF_ECO2): sensor.sensor_schema( + cv.Optional(CONF_ECO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_TVOC): sensor.sensor_schema( + cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_AQI): sensor.sensor_schema( + cv.Optional(CONF_AQI): sensor.sensor_schema( icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, @@ -62,12 +62,15 @@ async def to_code_base(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - sens = await sensor.new_sensor(config[CONF_ECO2]) - cg.add(var.set_co2(sens)) - sens = await sensor.new_sensor(config[CONF_TVOC]) - cg.add(var.set_tvoc(sens)) - sens = await sensor.new_sensor(config[CONF_AQI]) - cg.add(var.set_aqi(sens)) + if eco2_config := config.get(CONF_ECO2): + sens = await sensor.new_sensor(eco2_config) + cg.add(var.set_co2(sens)) + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) + cg.add(var.set_tvoc(sens)) + if aqi_config := config.get(CONF_AQI): + sens = await sensor.new_sensor(aqi_config) + cg.add(var.set_aqi(sens)) if compensation_config := config.get(CONF_COMPENSATION): sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) diff --git a/esphome/components/hte501/sensor.py b/esphome/components/hte501/sensor.py index 7eef641c4a..17ae3a3d1b 100644 --- a/esphome/components/hte501/sensor.py +++ b/esphome/components/hte501/sensor.py @@ -25,13 +25,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HTE501Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, @@ -49,10 +49,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/hyt271/sensor.py b/esphome/components/hyt271/sensor.py index 22364ce854..bf37646d4f 100644 --- a/esphome/components/hyt271/sensor.py +++ b/esphome/components/hyt271/sensor.py @@ -23,13 +23,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HYT271Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, @@ -47,10 +47,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity(sens)) diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 10428b1e4a..106636a6ba 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MHZ19Component), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, @@ -61,16 +61,20 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_AUTOMATIC_BASELINE_CALIBRATION in config: - cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION])) + if ( + automatic_baseline_calibration := config.get( + CONF_AUTOMATIC_BASELINE_CALIBRATION + ) + ) is not None: + cg.add(var.set_abc_enabled(automatic_baseline_calibration)) cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py index 168ca0c5c8..dfb6083bef 100644 --- a/esphome/components/ms5611/sensor.py +++ b/esphome/components/ms5611/sensor.py @@ -24,13 +24,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MS5611Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, icon=ICON_GAUGE, accuracy_decimals=1, @@ -49,10 +49,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure) cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ms8607/sensor.py b/esphome/components/ms8607/sensor.py index 7ed7c61750..038f17190a 100644 --- a/esphome/components/ms8607/sensor.py +++ b/esphome/components/ms8607/sensor.py @@ -29,19 +29,19 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MS8607Component), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=2, # Resolution: 0.01 device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_PRESSURE): sensor.sensor_schema( + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, accuracy_decimals=2, # Resolution: 0.016 device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=2, # Resolution: 0.04 device_class=DEVICE_CLASS_HUMIDITY, diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index cd6d85c12b..2eb2617e30 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -38,7 +38,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SenseAirComponent), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, @@ -57,8 +57,8 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 8c92f55ef7..848e4e9f9f 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -37,14 +37,14 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SGP30Component), - cv.Required(CONF_ECO2): sensor.sensor_schema( + cv.Optional(CONF_ECO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_TVOC): sensor.sensor_schema( + cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, @@ -86,32 +86,30 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_ECO2 in config: - sens = await sensor.new_sensor(config[CONF_ECO2]) + if eco2_config := config.get(CONF_ECO2): + sens = await sensor.new_sensor(eco2_config) cg.add(var.set_eco2_sensor(sens)) - if CONF_TVOC in config: - sens = await sensor.new_sensor(config[CONF_TVOC]) + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) cg.add(var.set_tvoc_sensor(sens)) - if CONF_ECO2_BASELINE in config: - sens = await sensor.new_sensor(config[CONF_ECO2_BASELINE]) + if eco2_baseline_config := config.get(CONF_ECO2_BASELINE): + sens = await sensor.new_sensor(eco2_baseline_config) cg.add(var.set_eco2_baseline_sensor(sens)) - if CONF_TVOC_BASELINE in config: - sens = await sensor.new_sensor(config[CONF_TVOC_BASELINE]) + if tvoc_baseline_config := config.get(CONF_TVOC_BASELINE): + sens = await sensor.new_sensor(tvoc_baseline_config) cg.add(var.set_tvoc_baseline_sensor(sens)) - if CONF_STORE_BASELINE in config: - cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + if (store_baseline := config.get(CONF_STORE_BASELINE)) is not None: + cg.add(var.set_store_baseline(store_baseline)) - if CONF_BASELINE in config: - baseline_config = config[CONF_BASELINE] + if baseline_config := config.get(CONF_BASELINE): cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE])) cg.add(var.set_tvoc_baseline(baseline_config[CONF_TVOC_BASELINE])) - if CONF_COMPENSATION in config: - compensation_config = config[CONF_COMPENSATION] + if compensation_config := config.get(CONF_COMPENSATION): sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) cg.add(var.set_humidity_sensor(sens)) sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index bb83ee4482..fdb1344fb7 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -26,13 +26,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SHTCXComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, @@ -50,10 +50,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py index 6df40497a9..9315e4a669 100644 --- a/esphome/components/t6615/sensor.py +++ b/esphome/components/t6615/sensor.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(T6615Component), - cv.Required(CONF_CO2): sensor.sensor_schema( + cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, accuracy_decimals=0, device_class=DEVICE_CLASS_CARBON_DIOXIDE, @@ -41,6 +41,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index c139c56ce4..1c78833500 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -63,7 +63,8 @@ void T6615Component::loop() { case T6615Command::GET_PPM: { const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]); ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); - this->co2_sensor_->publish_state(ppm); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); break; } default: From 2341ff651a0ccdee8e22d329287977723adba770 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 00:16:08 -0500 Subject: [PATCH 131/193] Use fixed buffer for plaintext protocol like noise protocol (#8800) --- esphome/components/api/api_frame_helper.cpp | 59 +++++++++++++++++---- esphome/components/api/api_frame_helper.h | 14 ++++- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 31b0732275..f251ceb6e4 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -830,6 +830,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // read header while (!rx_header_parsed_) { uint8_t data; + // Reading one byte at a time is fastest in practice for ESP32 when + // there is no data on the wire (which is the common case). + // This results in faster failure detection compared to + // attempting to read multiple bytes at once. ssize_t received = socket_->read(&data, 1); if (received == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { @@ -843,27 +847,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { HELPER_LOG("Connection closed"); return APIError::CONNECTION_CLOSED; } - rx_header_buf_.push_back(data); - // try parse header - if (rx_header_buf_[0] != 0x00) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); - return APIError::BAD_INDICATOR; + // Successfully read a byte + + // Process byte according to current buffer position + if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte) + if (data != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", data); + return APIError::BAD_INDICATOR; + } + // We don't store the indicator byte, just increment position + rx_header_buf_pos_ = 1; // Set to 1 directly + continue; // Need more bytes before we can parse } - size_t i = 1; + // Check buffer overflow before storing + if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed) + state_ = State::FAILED; + HELPER_LOG("Header buffer overflow"); + return APIError::BAD_DATA_PACKET; + } + + // Store byte in buffer (adjust index to account for skipped indicator byte) + rx_header_buf_[rx_header_buf_pos_ - 1] = data; + + // Increment position after storing + rx_header_buf_pos_++; + + // Case 3: If we only have one varint byte, we need more + if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte + continue; // Need more bytes before we can parse + } + + // At this point, we have at least 3 bytes total: + // - Validated indicator byte (0x00) but not stored + // - At least 2 bytes in the buffer for the varints + // Buffer layout: + // First 1-3 bytes: Message size varint (variable length) + // - 2 bytes would only allow up to 16383, which is less than noise's 65535 + // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise + // Remaining 1-2 bytes: Message type varint (variable length) + // We now attempt to parse both varints. If either is incomplete, + // we'll continue reading more bytes. + uint32_t consumed = 0; - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed); if (!msg_size_varint.has_value()) { // not enough data there yet continue; } - i += consumed; rx_header_parsed_len_ = msg_size_varint->as_uint32(); - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed); if (!msg_type_varint.has_value()) { // not enough data there yet continue; @@ -909,7 +946,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // consume msg rx_buf_ = {}; rx_buf_len_ = 0; - rx_header_buf_.clear(); + rx_header_buf_pos_ = 0; rx_header_parsed_ = false; return APIError::OK; } diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 59f3cf7471..db506ea1ce 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -119,6 +119,9 @@ class APINoiseFrameHelper : public APIFrameHelper { std::unique_ptr socket_; std::string info_; + // Fixed-size header buffer for noise protocol: + // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) + // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase uint8_t rx_header_buf_[3]; size_t rx_header_buf_len_ = 0; std::vector rx_buf_; @@ -179,7 +182,16 @@ class APIPlaintextFrameHelper : public APIFrameHelper { std::unique_ptr socket_; std::string info_; - std::vector rx_header_buf_; + // Fixed-size header buffer for plaintext protocol: + // We only need space for the two varints since we validate the indicator byte separately. + // To match noise protocol's maximum message size (65535), we need: + // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint + // + // While varints could theoretically be up to 10 bytes each for 64-bit values, + // attempting to process messages with headers that large would likely crash the + // ESP32 due to memory constraints. + uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type) + uint8_t rx_header_buf_pos_ = 0; bool rx_header_parsed_ = false; uint32_t rx_header_parsed_type_ = 0; uint32_t rx_header_parsed_len_ = 0; From c7e62d1279a179be0f51f62c4f8c091001ef4598 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 00:16:25 -0500 Subject: [PATCH 132/193] Optimize protobuf varint decoder for ESPHome use case (#8791) --- esphome/components/api/proto.h | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b8ee6b7920..e110a58eda 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -20,16 +20,26 @@ class ProtoVarInt { explicit ProtoVarInt(uint64_t value) : value_(value) {} static optional parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { - if (consumed != nullptr) - *consumed = 0; - - if (len == 0) + if (len == 0) { + if (consumed != nullptr) + *consumed = 0; return {}; + } - uint64_t result = 0; - uint8_t bitpos = 0; + // Most common case: single-byte varint (values 0-127) + if ((buffer[0] & 0x80) == 0) { + if (consumed != nullptr) + *consumed = 1; + return ProtoVarInt(buffer[0]); + } - for (uint32_t i = 0; i < len; i++) { + // General case for multi-byte varints + // Since we know buffer[0]'s high bit is set, initialize with its value + uint64_t result = buffer[0] & 0x7F; + uint8_t bitpos = 7; + + // Start from the second byte since we've already processed the first + for (uint32_t i = 1; i < len; i++) { uint8_t val = buffer[i]; result |= uint64_t(val & 0x7F) << uint64_t(bitpos); bitpos += 7; @@ -40,7 +50,9 @@ class ProtoVarInt { } } - return {}; + if (consumed != nullptr) + *consumed = 0; + return {}; // Incomplete or invalid varint } uint32_t as_uint32() const { return this->value_; } From 41f860c2a3655c2992a9a22794f44ef239234cb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 04:36:28 -0500 Subject: [PATCH 133/193] Logger Recursion Guard per Task on ESP32 (#8765) --- esphome/components/logger/__init__.py | 1 + esphome/components/logger/logger.cpp | 47 +++++++++++---------- esphome/components/logger/logger.h | 60 ++++++++++++++++++++++++--- 3 files changed, 82 insertions(+), 26 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 01e75a424d..4698c1d9f1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -254,6 +254,7 @@ async def to_code(config): config[CONF_TX_BUFFER_SIZE], ) if CORE.is_esp32: + cg.add(log.create_pthread_key()) task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 812a7cc16d..0ad909cb07 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -14,20 +14,27 @@ namespace logger { static const char *const TAG = "logger"; #ifdef USE_ESP32 -// Implementation for ESP32 (multi-core with atomic support) -// Main thread: synchronous logging with direct buffer access -// Other threads: console output with stack buffer, callbacks via async buffer +// Implementation for ESP32 (multi-task platform with task-specific tracking) +// Main task always uses direct buffer access for console output and callbacks +// Other tasks: +// - With task log buffer: stack buffer for console output, async buffer for callbacks +// - Without task log buffer: only console output, no callbacks void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed)) + if (level > this->level_for(tag)) return; - recursion_guard_.store(true, std::memory_order_relaxed); TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); + bool is_main_task = (current_task == main_task_); - // For main task: call log_message_to_buffer_and_send_ which does console and callback logging - if (current_task == main_task_) { + // Check and set recursion guard - uses pthread TLS for per-task state + if (this->check_and_set_task_log_recursion_(is_main_task)) { + return; // Recursion detected + } + + // Main task uses the shared buffer for efficiency + if (is_main_task) { this->log_message_to_buffer_and_send_(level, tag, line, format, args); - recursion_guard_.store(false, std::memory_order_release); + this->reset_task_log_recursion_(is_main_task); return; } @@ -51,23 +58,21 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * } #endif // USE_ESPHOME_TASK_LOG_BUFFER - recursion_guard_.store(false, std::memory_order_release); + // Reset the recursion guard for this task + this->reset_task_log_recursion_(is_main_task); } -#endif // USE_ESP32 - -#ifndef USE_ESP32 -// Implementation for platforms that do not support atomic operations -// or have to consider logging in other tasks +#else +// Implementation for all other platforms void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag) || recursion_guard_) + if (level > this->level_for(tag) || global_recursion_guard_) return; - recursion_guard_ = true; + global_recursion_guard_ = true; // Format and send to both console and callbacks this->log_message_to_buffer_and_send_(level, tag, line, format, args); - recursion_guard_ = false; + global_recursion_guard_ = false; } #endif // !USE_ESP32 @@ -76,10 +81,10 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT - if (level > this->level_for(tag) || recursion_guard_) + if (level > this->level_for(tag) || global_recursion_guard_) return; - recursion_guard_ = true; + global_recursion_guard_ = true; this->tx_buffer_at_ = 0; // Copy format string from progmem @@ -91,7 +96,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr // Buffer full from copying format if (this->tx_buffer_at_ >= this->tx_buffer_size_) { - recursion_guard_ = false; // Make sure to reset the recursion guard before returning + global_recursion_guard_ = false; // Make sure to reset the recursion guard before returning return; } @@ -107,7 +112,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); - recursion_guard_ = false; + global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8619cc0992..5c53c4d40c 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -3,7 +3,7 @@ #include #include #ifdef USE_ESP32 -#include +#include #endif #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -84,6 +84,23 @@ enum UARTSelection { }; #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY +/** + * @brief Logger component for all ESPHome logging. + * + * This class implements a multi-platform logging system with protection against recursion. + * + * Recursion Protection Strategy: + * - On ESP32: Uses task-specific recursion guards + * * Main task: Uses a dedicated boolean member variable for efficiency + * * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state + * - On other platforms: Uses a simple global recursion guard + * + * We use pthread TLS via pthread_key_create to create a unique key for storing + * task-specific recursion state, which: + * 1. Efficiently handles multiple tasks without locks or mutexes + * 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables + * 3. Avoids the limitations of the fixed FreeRTOS task local storage slots + */ class Logger : public Component { public: explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); @@ -102,6 +119,9 @@ class Logger : public Component { #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } #endif +#ifdef USE_ESP32 + void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. @@ -222,18 +242,22 @@ class Logger : public Component { std::map log_levels_{}; CallbackManager log_callback_{}; int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#ifdef USE_ESP32 - std::atomic recursion_guard_{false}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer #endif +#ifdef USE_ESP32 + // Task-specific recursion guards: + // - Main task uses a dedicated member variable for efficiency + // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create + bool main_task_recursion_guard_{false}; + pthread_key_t log_recursion_key_; #else - bool recursion_guard_{false}; + bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - void *main_task_ = nullptr; CallbackManager level_callback_{}; #if defined(USE_ESP32) || defined(USE_LIBRETINY) + void *main_task_ = nullptr; // Only used for thread name identification const char *HOT get_thread_name_() { TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); if (current_task == main_task_) { @@ -248,6 +272,32 @@ class Logger : public Component { } #endif +#ifdef USE_ESP32 + inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) { + if (is_main_task) { + const bool was_recursive = main_task_recursion_guard_; + main_task_recursion_guard_ = true; + return was_recursive; + } + + intptr_t current = (intptr_t) pthread_getspecific(log_recursion_key_); + if (current != 0) + return true; + + pthread_setspecific(log_recursion_key_, (void *) 1); + return false; + } + + inline void HOT reset_task_log_recursion_(bool is_main_task) { + if (is_main_task) { + main_task_recursion_guard_ = false; + return; + } + + pthread_setspecific(log_recursion_key_, (void *) 0); + } +#endif + inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, int *buffer_at, int buffer_size) { // Format header From 5a84bab9ec088b74fb96ff217d9483efaf89d70f Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Thu, 15 May 2025 11:45:07 +0200 Subject: [PATCH 134/193] [log] improve/refactor `log` (#8708) --- esphome/__main__.py | 30 +++++++++-------- esphome/config.py | 26 +++++++-------- esphome/log.py | 34 +++++++++---------- esphome/mqtt.py | 4 +-- esphome/wizard.py | 80 +++++++++++++++++++++++++-------------------- 5 files changed, 91 insertions(+), 83 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c78eda7e12..9f638456e6 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -43,7 +43,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, coroutine from esphome.helpers import get_bool_env, indent, is_ip_address -from esphome.log import Fore, color, setup_log +from esphome.log import AnsiFore, color, setup_log from esphome.util import ( get_serial_ports, list_yaml_files, @@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None): raise ValueError break except ValueError: - safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) + safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'")) return options[opt - 1][1] @@ -596,30 +596,30 @@ def command_update_all(args): click.echo(f"{half_line}{middle_text}{half_line}") for f in files: - print(f"Updating {color(Fore.CYAN, f)}") + print(f"Updating {color(AnsiFore.CYAN, f)}") print("-" * twidth) print() rc = run_external_process( "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" ) if rc == 0: - print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") + print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}") success[f] = True else: - print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") + print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}") success[f] = False print() print() print() - print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") + print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]") failed = 0 for f in files: if success[f]: - print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}") + print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}") else: - print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}") + print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}") failed += 1 return failed @@ -645,7 +645,7 @@ def command_rename(args, config): if c not in ALLOWED_NAME_CHARS: print( color( - Fore.BOLD_RED, + AnsiFore.BOLD_RED, f"'{c}' is an invalid character for names. Valid characters are: " f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", ) @@ -658,7 +658,9 @@ def command_rename(args, config): yaml = yaml_util.load_yaml(CORE.config_path) if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: print( - color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") + color( + AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed." + ) ) return 1 old_name = yaml[CONF_ESPHOME][CONF_NAME] @@ -681,7 +683,7 @@ def command_rename(args, config): ) > 1 ): - print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) + print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename")) return 1 new_raw = re.sub( @@ -693,7 +695,7 @@ def command_rename(args, config): new_path = os.path.join(CORE.config_dir, args.name + ".yaml") print( - f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" + f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}" ) print() @@ -702,7 +704,7 @@ def command_rename(args, config): rc = run_external_process("esphome", "config", new_path) if rc != 0: - print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes.")) os.remove(new_path) return 1 @@ -728,7 +730,7 @@ def command_rename(args, config): if CORE.config_path != new_path: os.remove(CORE.config_path) - print(color(Fore.BOLD_GREEN, "SUCCESS")) + print(color(AnsiFore.BOLD_GREEN, "SUCCESS")) print() return 0 diff --git a/esphome/config.py b/esphome/config.py index 09ee2a8f9b..4b26b33c78 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -28,7 +28,7 @@ import esphome.core.config as core_config import esphome.final_validate as fv from esphome.helpers import indent from esphome.loader import ComponentManifest, get_component, get_platform -from esphome.log import Fore, color +from esphome.log import AnsiFore, color from esphome.types import ConfigFragmentType, ConfigType from esphome.util import OrderedDict, safe_print from esphome.voluptuous_schema import ExtraKeysInvalid @@ -959,7 +959,7 @@ def line_info(config, path, highlight=True): if obj: mark = obj.start_mark source = f"[source {mark.document}:{mark.line + 1}]" - return color(Fore.CYAN, source) + return color(AnsiFore.CYAN, source) return "None" @@ -983,7 +983,7 @@ def dump_dict( if at_root: error = config.get_error_for_path(path) if error is not None: - ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" + ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n" if isinstance(conf, (list, tuple)): multiline = True @@ -995,11 +995,11 @@ def dump_dict( path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" + ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n" sep = "- " if config.is_in_error_path(path_): - sep = color(Fore.RED, sep) + sep = color(AnsiFore.RED, sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) @@ -1018,11 +1018,11 @@ def dump_dict( path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" + ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n" st = f"{k}: " if config.is_in_error_path(path_): - st = color(Fore.RED, st) + st = color(AnsiFore.RED, st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) @@ -1044,7 +1044,7 @@ def dump_dict( if len(conf) > 80: conf = f"|-\n{indent(conf)}" error = config.get_error_for_path(path) - col = Fore.BOLD_RED if error else Fore.KEEP + col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): @@ -1052,13 +1052,13 @@ def dump_dict( conf = f"!lambda |-\n{indent(str(conf.value))}" error = config.get_error_for_path(path) - col = Fore.BOLD_RED if error else Fore.KEEP + col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP ret += color(col, conf) elif conf is None: pass else: error = config.get_error_for_path(path) - col = Fore.BOLD_RED if error else Fore.KEEP + col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP ret += color(col, str(conf)) multiline = "\n" in ret @@ -1100,13 +1100,13 @@ def read_config(command_line_substitutions): if not CORE.verbose: res = strip_default_ids(res) - safe_print(color(Fore.BOLD_RED, "Failed config")) + safe_print(color(AnsiFore.BOLD_RED, "Failed config")) safe_print("") for path, domain in res.output_paths: if not res.is_in_error_path(path): continue - errstr = color(Fore.BOLD_RED, f"{domain}:") + errstr = color(AnsiFore.BOLD_RED, f"{domain}:") errline = line_info(res, path) if errline: errstr += f" {errline}" @@ -1121,7 +1121,7 @@ def read_config(command_line_substitutions): safe_print(indent("\n".join(split_dump[:i]))) for err in res.errors: - safe_print(color(Fore.BOLD_RED, err.msg)) + safe_print(color(AnsiFore.BOLD_RED, err.msg)) safe_print("") return None diff --git a/esphome/log.py b/esphome/log.py index 516f27be45..7e69a2fef8 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -1,9 +1,10 @@ +from enum import Enum import logging from esphome.core import CORE -class AnsiFore: +class AnsiFore(Enum): KEEP = "" BLACK = "\033[30m" RED = "\033[31m" @@ -26,7 +27,7 @@ class AnsiFore: BOLD_RESET = "\033[1;39m" -class AnsiStyle: +class AnsiStyle(Enum): BRIGHT = "\033[1m" BOLD = "\033[1m" DIM = "\033[2m" @@ -35,16 +36,10 @@ class AnsiStyle: RESET_ALL = "\033[0m" -Fore = AnsiFore() -Style = AnsiStyle() - - -def color(col: str, msg: str, reset: bool = True) -> bool: - if col and not col.startswith("\033["): - raise ValueError("Color must be value from esphome.log.Fore") - s = str(col) + msg +def color(col: AnsiFore, msg: str, reset: bool = True) -> str: + s = col.value + msg if reset and col: - s += str(Style.RESET_ALL) + s += AnsiStyle.RESET_ALL.value return s @@ -54,20 +49,21 @@ class ESPHomeLogFormatter(logging.Formatter): fmt += "%(levelname)s %(message)s" super().__init__(fmt=fmt, style="%") - def format(self, record): + # @override + def format(self, record: logging.LogRecord) -> str: formatted = super().format(record) prefix = { - "DEBUG": Fore.CYAN, - "INFO": Fore.GREEN, - "WARNING": Fore.YELLOW, - "ERROR": Fore.RED, - "CRITICAL": Fore.RED, + "DEBUG": AnsiFore.CYAN.value, + "INFO": AnsiFore.GREEN.value, + "WARNING": AnsiFore.YELLOW.value, + "ERROR": AnsiFore.RED.value, + "CRITICAL": AnsiFore.RED.value, }.get(record.levelname, "") - return f"{prefix}{formatted}{Style.RESET_ALL}" + return f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}" def setup_log( - log_level=logging.INFO, + log_level: int = logging.INFO, include_timestamp: bool = False, ) -> None: import colorama diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 2403a4a1d9..a420b5ba7f 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -28,7 +28,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import Fore, color +from esphome.log import AnsiFore, color from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -291,7 +291,7 @@ def get_fingerprint(config): sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}") + safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") safe_print( f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" ) diff --git a/esphome/wizard.py b/esphome/wizard.py index 8c5bd07e1f..ca987304e2 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -9,7 +9,7 @@ import esphome.config_validation as cv from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD from esphome.core import CORE from esphome.helpers import get_bool_env, write_file -from esphome.log import Fore, color +from esphome.log import AnsiFore, color from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_input, safe_print @@ -219,7 +219,7 @@ def wizard_write(path, **kwargs): elif board in rtl87xx_boards.BOARDS: platform = "RTL87XX" else: - safe_print(color(Fore.RED, f'The board "{board}" is unknown.')) + safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.')) return False kwargs["platform"] = platform hardware = kwargs["platform"] @@ -274,12 +274,12 @@ def wizard(path): if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( - f"Please make your configuration file {color(Fore.CYAN, path)} have the extension .yaml or .yml" + f"Please make your configuration file {color(AnsiFore.CYAN, path)} have the extension .yaml or .yml" ) return 1 if os.path.exists(path): safe_print( - f"Uh oh, it seems like {color(Fore.CYAN, path)} already exists, please delete that file first or chose another configuration file." + f"Uh oh, it seems like {color(AnsiFore.CYAN, path)} already exists, please delete that file first or chose another configuration file." ) return 2 @@ -298,17 +298,19 @@ def wizard(path): sleep(3.0) safe_print() safe_print_step(1, CORE_BIG) - safe_print(f"First up, please choose a {color(Fore.GREEN, 'name')} for your node.") + safe_print( + f"First up, please choose a {color(AnsiFore.GREEN, 'name')} for your node." + ) safe_print( "It should be a unique name that can be used to identify the device later." ) sleep(1) safe_print( - f"For example, I like calling the node in my living room {color(Fore.BOLD_WHITE, 'livingroom')}." + f"For example, I like calling the node in my living room {color(AnsiFore.BOLD_WHITE, 'livingroom')}." ) safe_print() sleep(1) - name = safe_input(color(Fore.BOLD_WHITE, "(name): ")) + name = safe_input(color(AnsiFore.BOLD_WHITE, "(name): ")) while True: try: @@ -317,7 +319,7 @@ def wizard(path): except vol.Invalid: safe_print( color( - Fore.RED, + AnsiFore.RED, f'Oh noes, "{name}" isn\'t a valid name. Names can only ' f"include numbers, lower-case letters and hyphens. ", ) @@ -325,11 +327,13 @@ def wizard(path): name = strip_accents(name).lower().replace(" ", "-") name = strip_accents(name).lower().replace("_", "-") name = "".join(c for c in name if c in ALLOWED_NAME_CHARS) - safe_print(f'Shall I use "{color(Fore.CYAN, name)}" as the name instead?') + safe_print( + f'Shall I use "{color(AnsiFore.CYAN, name)}" as the name instead?' + ) sleep(0.5) name = default_input("(name [{}]): ", name) - safe_print(f'Great! Your node is now called "{color(Fore.CYAN, name)}".') + safe_print(f'Great! Your node is now called "{color(AnsiFore.CYAN, name)}".') sleep(1) safe_print_step(2, ESP_BIG) safe_print( @@ -346,7 +350,7 @@ def wizard(path): sleep(0.5) safe_print() platform = safe_input( - color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ") + color(AnsiFore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ") ) try: platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper()) @@ -355,7 +359,9 @@ def wizard(path): safe_print( f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.' ) - safe_print(f"Thanks! You've chosen {color(Fore.CYAN, platform)} as your platform.") + safe_print( + f"Thanks! You've chosen {color(AnsiFore.CYAN, platform)} as your platform." + ) safe_print() sleep(1) @@ -376,27 +382,29 @@ def wizard(path): else: raise NotImplementedError("Unknown platform!") - safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.") + safe_print( + f"Next, I need to know what {color(AnsiFore.GREEN, 'board')} you're using." + ) sleep(0.5) - safe_print(f"Please go to {color(Fore.GREEN, board_link)} and choose a board.") + safe_print(f"Please go to {color(AnsiFore.GREEN, board_link)} and choose a board.") if platform == "ESP32": - safe_print(f"(Type {color(Fore.GREEN, 'esp01_1m')} for Sonoff devices)") + safe_print(f"(Type {color(AnsiFore.GREEN, 'esp01_1m')} for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link if platform == "ESP32": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcu-32s")}".') boards_list = esp32_boards.BOARDS.items() elif platform == "ESP8266": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcuv2")}".') boards_list = esp8266_boards.BOARDS.items() elif platform == "BK72XX": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".') boards_list = bk72xx_boards.BOARDS.items() elif platform == "RTL87XX": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".') boards_list = rtl87xx_boards.BOARDS.items() elif platform == "RP2040": - safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "rpipicow")}".') boards_list = rp2040_boards.BOARDS.items() else: @@ -409,19 +417,21 @@ def wizard(path): boards.append(board_id) while True: - board = safe_input(color(Fore.BOLD_WHITE, "(board): ")) + board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: safe_print( - color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.') + color( + AnsiFore.RED, f'Sorry, I don\'t think the board "{board}" exists.' + ) ) safe_print() sleep(0.25) safe_print() - safe_print(f"Way to go! You've chosen {color(Fore.CYAN, board)} as your board.") + safe_print(f"Way to go! You've chosen {color(AnsiFore.CYAN, board)} as your board.") safe_print() sleep(1) @@ -432,19 +442,19 @@ def wizard(path): safe_print() sleep(1) safe_print( - f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" + f"First, what's the {color(AnsiFore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" ) sleep(1.5) - safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "Abraham Linksys")}".') while True: - ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): ")) + ssid = safe_input(color(AnsiFore.BOLD_WHITE, "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: safe_print( color( - Fore.RED, + AnsiFore.RED, f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.', ) ) @@ -452,18 +462,18 @@ def wizard(path): sleep(1) safe_print( - f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.' + f'Thank you very much! You\'ve just chosen "{color(AnsiFore.CYAN, ssid)}" as your SSID.' ) safe_print() sleep(0.75) safe_print( - f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" + f"Now please state the {color(AnsiFore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" ) safe_print() - safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"') + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "PASSWORD42")}"') sleep(0.5) - psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): ")) + psk = safe_input(color(AnsiFore.BOLD_WHITE, "(PSK): ")) safe_print( "Perfect! WiFi is now set up (you can create static IPs and so on later)." ) @@ -475,12 +485,12 @@ def wizard(path): "(over the air) and integrates into Home Assistant with a native API." ) safe_print( - f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?" + f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?" ) safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = safe_input(color(Fore.BOLD_WHITE, "(password): ")) + password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) else: ssid, password, psk = "", "", "" @@ -497,8 +507,8 @@ def wizard(path): safe_print() safe_print( - color(Fore.CYAN, "DONE! I've now written a new configuration file to ") - + color(Fore.BOLD_CYAN, path) + color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ") + + color(AnsiFore.BOLD_CYAN, path) ) safe_print() safe_print("Next steps:") From c50e33f531aed5536267c1633d37ac6a98a5fcfe Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Thu, 15 May 2025 12:07:41 +0200 Subject: [PATCH 135/193] [gps] update lib, improve code/tests/config (#8768) --- CODEOWNERS | 2 +- esphome/components/gps/__init__.py | 53 +++++++++++++++++++++--------- esphome/components/gps/gps.cpp | 50 ++++++++++++++++++---------- esphome/components/gps/gps.h | 31 ++++++++--------- platformio.ini | 2 +- tests/components/gps/common.yaml | 14 ++++++++ 6 files changed, 102 insertions(+), 50 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ddd0494a3c..a6e08f225d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,7 +169,7 @@ esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gpio/one_wire/* @ssieb -esphome/components/gps/* @coogle +esphome/components/gps/* @coogle @ximex esphome/components/graph/* @synco esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 88e6f0fd9b..7ccd82dec3 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -9,23 +9,32 @@ from esphome.const import ( CONF_LONGITUDE, CONF_SATELLITES, CONF_SPEED, + DEVICE_CLASS_SPEED, STATE_CLASS_MEASUREMENT, UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, ) +CONF_GPS_ID = "gps_id" +CONF_HDOP = "hdop" + +ICON_ALTIMETER = "mdi:altimeter" +ICON_COMPASS = "mdi:compass" +ICON_LATITUDE = "mdi:latitude" +ICON_LONGITUDE = "mdi:longitude" +ICON_SATELLITE = "mdi:satellite-variant" +ICON_SPEEDOMETER = "mdi:speedometer" + DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor"] -CODEOWNERS = ["@coogle"] +CODEOWNERS = ["@coogle", "@ximex"] gps_ns = cg.esphome_ns.namespace("gps") GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) GPSListener = gps_ns.class_("GPSListener") -CONF_GPS_ID = "gps_id" -CONF_HDOP = "hdop" MULTI_CONF = True CONFIG_SCHEMA = cv.All( cv.Schema( @@ -33,25 +42,37 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(GPS), cv.Optional(CONF_LATITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, + icon=ICON_LATITUDE, accuracy_decimals=6, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, + icon=ICON_LONGITUDE, accuracy_decimals=6, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + icon=ICON_SPEEDOMETER, accuracy_decimals=3, + device_class=DEVICE_CLASS_SPEED, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, + icon=ICON_COMPASS, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, + icon=ICON_ALTIMETER, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + icon=ICON_SATELLITE, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), @@ -73,28 +94,28 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_LATITUDE in config: - sens = await sensor.new_sensor(config[CONF_LATITUDE]) + if latitude_config := config.get(CONF_LATITUDE): + sens = await sensor.new_sensor(latitude_config) cg.add(var.set_latitude_sensor(sens)) - if CONF_LONGITUDE in config: - sens = await sensor.new_sensor(config[CONF_LONGITUDE]) + if longitude_config := config.get(CONF_LONGITUDE): + sens = await sensor.new_sensor(longitude_config) cg.add(var.set_longitude_sensor(sens)) - if CONF_SPEED in config: - sens = await sensor.new_sensor(config[CONF_SPEED]) + if speed_config := config.get(CONF_SPEED): + sens = await sensor.new_sensor(speed_config) cg.add(var.set_speed_sensor(sens)) - if CONF_COURSE in config: - sens = await sensor.new_sensor(config[CONF_COURSE]) + if course_config := config.get(CONF_COURSE): + sens = await sensor.new_sensor(course_config) cg.add(var.set_course_sensor(sens)) - if CONF_ALTITUDE in config: - sens = await sensor.new_sensor(config[CONF_ALTITUDE]) + if altitude_config := config.get(CONF_ALTITUDE): + sens = await sensor.new_sensor(altitude_config) cg.add(var.set_altitude_sensor(sens)) - if CONF_SATELLITES in config: - sens = await sensor.new_sensor(config[CONF_SATELLITES]) + if satellites_config := config.get(CONF_SATELLITES): + sens = await sensor.new_sensor(satellites_config) cg.add(var.set_satellites_sensor(sens)) if hdop_config := config.get(CONF_HDOP): @@ -102,4 +123,4 @@ async def to_code(config): cg.add(var.set_hdop_sensor(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus - cg.add_library("mikalhart/TinyGPSPlus", "1.0.2") + cg.add_library("mikalhart/TinyGPSPlus", "1.1.0") diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index e54afdb07e..9dcb351b39 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -10,6 +10,17 @@ static const char *const TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::dump_config() { + ESP_LOGCONFIG(TAG, "GPS:"); + LOG_SENSOR(" ", "Latitude", this->latitude_sensor_); + LOG_SENSOR(" ", "Longitude", this->longitude_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Course", this->course_sensor_); + LOG_SENSOR(" ", "Altitude", this->altitude_sensor_); + LOG_SENSOR(" ", "Satellites", this->satellites_sensor_); + LOG_SENSOR(" ", "HDOP", this->hdop_sensor_); +} + void GPS::update() { if (this->latitude_sensor_ != nullptr) this->latitude_sensor_->publish_state(this->latitude_); @@ -34,40 +45,45 @@ void GPS::update() { } void GPS::loop() { - while (this->available() && !this->has_time_) { + while (this->available() > 0 && !this->has_time_) { if (this->tiny_gps_.encode(this->read())) { - if (tiny_gps_.location.isUpdated()) { - this->latitude_ = tiny_gps_.location.lat(); - this->longitude_ = tiny_gps_.location.lng(); + if (this->tiny_gps_.location.isUpdated()) { + this->latitude_ = this->tiny_gps_.location.lat(); + this->longitude_ = this->tiny_gps_.location.lng(); ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %f", this->latitude_); - ESP_LOGD(TAG, " Lon: %f", this->longitude_); + ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_); + ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_); } - if (tiny_gps_.speed.isUpdated()) { - this->speed_ = tiny_gps_.speed.kmph(); + if (this->tiny_gps_.speed.isUpdated()) { + this->speed_ = this->tiny_gps_.speed.kmph(); ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); } - if (tiny_gps_.course.isUpdated()) { - this->course_ = tiny_gps_.course.deg(); + + if (this->tiny_gps_.course.isUpdated()) { + this->course_ = this->tiny_gps_.course.deg(); ESP_LOGD(TAG, "Course: %.2f °", this->course_); } - if (tiny_gps_.altitude.isUpdated()) { - this->altitude_ = tiny_gps_.altitude.meters(); + + if (this->tiny_gps_.altitude.isUpdated()) { + this->altitude_ = this->tiny_gps_.altitude.meters(); ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); } - if (tiny_gps_.satellites.isUpdated()) { - this->satellites_ = tiny_gps_.satellites.value(); + + if (this->tiny_gps_.satellites.isUpdated()) { + this->satellites_ = this->tiny_gps_.satellites.value(); ESP_LOGD(TAG, "Satellites: %d", this->satellites_); } - if (tiny_gps_.hdop.isUpdated()) { - this->hdop_ = tiny_gps_.hdop.hdop(); + + if (this->tiny_gps_.hdop.isUpdated()) { + this->hdop_ = this->tiny_gps_.hdop.hdop(); ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_); } - for (auto *listener : this->listeners_) + for (auto *listener : this->listeners_) { listener->on_update(this->tiny_gps_); + } } } } diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index a400820738..7bc23ed1e0 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -5,7 +5,7 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" -#include +#include #include @@ -27,13 +27,13 @@ class GPSListener { class GPS : public PollingComponent, public uart::UARTDevice { public: - void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; } - void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; } - void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; } - void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } - void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } - void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } - void set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; } + void set_latitude_sensor(sensor::Sensor *latitude_sensor) { this->latitude_sensor_ = latitude_sensor; } + void set_longitude_sensor(sensor::Sensor *longitude_sensor) { this->longitude_sensor_ = longitude_sensor; } + void set_speed_sensor(sensor::Sensor *speed_sensor) { this->speed_sensor_ = speed_sensor; } + void set_course_sensor(sensor::Sensor *course_sensor) { this->course_sensor_ = course_sensor; } + void set_altitude_sensor(sensor::Sensor *altitude_sensor) { this->altitude_sensor_ = altitude_sensor; } + void set_satellites_sensor(sensor::Sensor *satellites_sensor) { this->satellites_sensor_ = satellites_sensor; } + void set_hdop_sensor(sensor::Sensor *hdop_sensor) { this->hdop_sensor_ = hdop_sensor; } void register_listener(GPSListener *listener) { listener->parent_ = this; @@ -41,19 +41,20 @@ class GPS : public PollingComponent, public uart::UARTDevice { } float get_setup_priority() const override { return setup_priority::HARDWARE; } + void dump_config() override; void loop() override; void update() override; TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: - float latitude_ = NAN; - float longitude_ = NAN; - float speed_ = NAN; - float course_ = NAN; - float altitude_ = NAN; - int satellites_ = 0; - double hdop_ = NAN; + float latitude_{NAN}; + float longitude_{NAN}; + float speed_{NAN}; + float course_{NAN}; + float altitude_{NAN}; + uint16_t satellites_{0}; + float hdop_{NAN}; sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr}; diff --git a/platformio.ini b/platformio.ini index ccfd52c3ca..292188c6fa 100644 --- a/platformio.ini +++ b/platformio.ini @@ -64,7 +64,7 @@ lib_deps = heman/AsyncMqttClient-esphome@1.0.0 ; mqtt esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base - mikalhart/TinyGPSPlus@1.0.2 ; gps + mikalhart/TinyGPSPlus@1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr diff --git a/tests/components/gps/common.yaml b/tests/components/gps/common.yaml index fc8228c909..53dc67e457 100644 --- a/tests/components/gps/common.yaml +++ b/tests/components/gps/common.yaml @@ -6,6 +6,20 @@ uart: parity: EVEN gps: + latitude: + name: "Latitude" + longitude: + name: "Longitude" + altitude: + name: "Altitude" + speed: + name: "Speed" + course: + name: "Course" + satellites: + name: "Satellites" + hdop: + name: "HDOP" time: - platform: gps From d847b345b8ae233088e0cdeab42218a3ce1901b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 18:42:54 -0500 Subject: [PATCH 136/193] Fix ESP32 Camera class inheritance (#8811) --- esphome/components/esp32_camera/esp32_camera.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 71f47d3c06..d5fe48c2a7 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -106,7 +106,7 @@ class CameraImageReader { }; /* ---------------- ESP32Camera class ---------------- */ -class ESP32Camera : public Component, public EntityBase { +class ESP32Camera : public EntityBase, public Component { public: ESP32Camera(); From d8516cfabb118434f84fa49aa50dbf5e02d2de87 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 17 May 2025 22:05:03 -0500 Subject: [PATCH 137/193] [sen5x] Fix validation for values read from hardware (#8769) --- esphome/components/sen5x/sen5x.cpp | 89 ++++++++++++++++++------------ 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 0efc961943..f29ba18d6e 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -25,6 +25,10 @@ static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2; static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; +static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // used for VOC and NOx index values +static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor +static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor + void SEN5XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up sen5x..."); @@ -88,8 +92,9 @@ void SEN5XComponent::setup() { product_name_.push_back(current_char); // second char current_char = *current_int & 0xFF; - if (current_char) + if (current_char) { product_name_.push_back(current_char); + } } current_int++; } while (current_char && --max); @@ -271,10 +276,10 @@ void SEN5XComponent::dump_config() { ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); break; case MEDIUM_ACCELERATION: - ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode"); + ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode"); break; case HIGH_ACCELERATION: - ESP_LOGCONFIG(TAG, " High RH/T accelertion mode"); + ESP_LOGCONFIG(TAG, " High RH/T acceleration mode"); break; } } @@ -337,47 +342,61 @@ void SEN5XComponent::update() { ESP_LOGD(TAG, "read data error (%d)", this->last_error_); return; } - float pm_1_0 = measurements[0] / 10.0; - if (measurements[0] == 0xFFFF) - pm_1_0 = NAN; - float pm_2_5 = measurements[1] / 10.0; - if (measurements[1] == 0xFFFF) - pm_2_5 = NAN; - float pm_4_0 = measurements[2] / 10.0; - if (measurements[2] == 0xFFFF) - pm_4_0 = NAN; - float pm_10_0 = measurements[3] / 10.0; - if (measurements[3] == 0xFFFF) - pm_10_0 = NAN; - float humidity = measurements[4] / 100.0; - if (measurements[4] == 0xFFFF) - humidity = NAN; - float temperature = (int16_t) measurements[5] / 200.0; - if (measurements[5] == 0xFFFF) - temperature = NAN; - float voc = measurements[6] / 10.0; - if (measurements[6] == 0xFFFF) - voc = NAN; - float nox = measurements[7] / 10.0; - if (measurements[7] == 0xFFFF) - nox = NAN; - if (this->pm_1_0_sensor_ != nullptr) + ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]); + float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f; + + ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]); + float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f; + + ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]); + float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f; + + ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]); + float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f; + + ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]); + float humidity = measurements[4] == INT16_MAX ? NAN : static_cast(measurements[4]) / 100.0f; + + ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]); + float temperature = measurements[5] == INT16_MAX ? NAN : static_cast(measurements[5]) / 200.0f; + + ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]); + int16_t voc_idx = static_cast(measurements[6]); + float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE) + ? NAN + : static_cast(voc_idx) / 10.0f; + + ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]); + int16_t nox_idx = static_cast(measurements[7]); + float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE) + ? NAN + : static_cast(nox_idx) / 10.0f; + + if (this->pm_1_0_sensor_ != nullptr) { this->pm_1_0_sensor_->publish_state(pm_1_0); - if (this->pm_2_5_sensor_ != nullptr) + } + if (this->pm_2_5_sensor_ != nullptr) { this->pm_2_5_sensor_->publish_state(pm_2_5); - if (this->pm_4_0_sensor_ != nullptr) + } + if (this->pm_4_0_sensor_ != nullptr) { this->pm_4_0_sensor_->publish_state(pm_4_0); - if (this->pm_10_0_sensor_ != nullptr) + } + if (this->pm_10_0_sensor_ != nullptr) { this->pm_10_0_sensor_->publish_state(pm_10_0); - if (this->temperature_sensor_ != nullptr) + } + if (this->temperature_sensor_ != nullptr) { this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) + } + if (this->humidity_sensor_ != nullptr) { this->humidity_sensor_->publish_state(humidity); - if (this->voc_sensor_ != nullptr) + } + if (this->voc_sensor_ != nullptr) { this->voc_sensor_->publish_state(voc); - if (this->nox_sensor_ != nullptr) + } + if (this->nox_sensor_ != nullptr) { this->nox_sensor_->publish_state(nox); + } this->status_clear_warning(); }); } From 59295a615e728465ba64b156f1bb5bef2ff9dd00 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Sun, 18 May 2025 09:11:09 +0600 Subject: [PATCH 138/193] Fix the case of single error (#8824) --- esphome/voluptuous_schema.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 15f9206f21..8fb966e3b2 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -15,7 +15,9 @@ class ExtraKeysInvalid(vol.Invalid): def ensure_multiple_invalid(err): if isinstance(err, vol.MultipleInvalid): return err - return vol.MultipleInvalid(err) + if isinstance(err, list): + return vol.MultipleInvalid(err) + return vol.MultipleInvalid([err]) # pylint: disable=protected-access, unidiomatic-typecheck From 868f5ff20c6570c52e26023a7f379113e98a5209 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 18 May 2025 13:30:08 +1000 Subject: [PATCH 139/193] Revert "[binary_sensor] initial state refactor" (#8828) --- .../binary_sensor/binary_sensor.cpp | 16 ++++--- .../components/binary_sensor/binary_sensor.h | 2 +- esphome/components/binary_sensor/filter.cpp | 42 +++++++++---------- esphome/components/binary_sensor/filter.h | 20 ++++----- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 30fbe4f0b4..20604a0b7e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -15,17 +15,21 @@ void BinarySensor::publish_state(bool state) { if (!this->publish_dedup_.next(state)) return; if (this->filter_list_ == nullptr) { - this->send_state_internal(state); + this->send_state_internal(state, false); } else { - this->filter_list_->input(state); + this->filter_list_->input(state, false); } } void BinarySensor::publish_initial_state(bool state) { - this->has_state_ = false; - this->publish_state(state); + if (!this->publish_dedup_.next(state)) + return; + if (this->filter_list_ == nullptr) { + this->send_state_internal(state, true); + } else { + this->filter_list_->input(state, true); + } } -void BinarySensor::send_state_internal(bool state) { - bool is_initial = !this->has_state_; +void BinarySensor::send_state_internal(bool state, bool is_initial) { if (is_initial) { ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); } else { diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 9ba7aeeeff..57cae9e2f5 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool state); + void send_state_internal(bool state, bool is_initial); /// Return whether this binary sensor has outputted a state. virtual bool has_state() const; diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index fd6cc31008..8f94b108ac 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -9,37 +9,37 @@ namespace binary_sensor { static const char *const TAG = "sensor.filter"; -void Filter::output(bool value) { +void Filter::output(bool value, bool is_initial) { if (!this->dedup_.next(value)) return; if (this->next_ == nullptr) { - this->parent_->send_state_internal(value); + this->parent_->send_state_internal(value, is_initial); } else { - this->next_->input(value); + this->next_->input(value, is_initial); } } -void Filter::input(bool value) { - auto b = this->new_value(value); +void Filter::input(bool value, bool is_initial) { + auto b = this->new_value(value, is_initial); if (b.has_value()) { - this->output(*b); + this->output(*b, is_initial); } } -optional DelayedOnOffFilter::new_value(bool value) { +optional DelayedOnOffFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); + this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); } else { - this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); + this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); } return {}; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOnFilter::new_value(bool value) { +optional DelayedOnFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); + this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); return {}; } else { this->cancel_timeout("ON"); @@ -49,9 +49,9 @@ optional DelayedOnFilter::new_value(bool value) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOffFilter::new_value(bool value) { +optional DelayedOffFilter::new_value(bool value, bool is_initial) { if (!value) { - this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); + this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); return {}; } else { this->cancel_timeout("OFF"); @@ -61,11 +61,11 @@ optional DelayedOffFilter::new_value(bool value) { float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional InvertFilter::new_value(bool value) { return !value; } +optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} -optional AutorepeatFilter::new_value(bool value) { +optional AutorepeatFilter::new_value(bool value, bool is_initial) { if (value) { // Ignore if already running if (this->active_timing_ != 0) @@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() { void AutorepeatFilter::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val); + this->output(val, false); // This is at least the second one so not initial this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); } @@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} -optional LambdaFilter::new_value(bool value) { return this->f_(value); } +optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } -optional SettleFilter::new_value(bool value) { +optional SettleFilter::new_value(bool value, bool is_initial) { if (!this->steady_) { - this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { + this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { this->steady_ = true; - this->output(value); + this->output(value, is_initial); }); return {}; } else { this->steady_ = false; - this->output(value); + this->output(value, is_initial); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 65838da49d..f7342db2fb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -14,11 +14,11 @@ class BinarySensor; class Filter { public: - virtual optional new_value(bool value) = 0; + virtual optional new_value(bool value, bool is_initial) = 0; - void input(bool value); + void input(bool value, bool is_initial); - void output(bool value); + void output(bool value, bool is_initial); protected: friend BinarySensor; @@ -30,7 +30,7 @@ class Filter { class DelayedOnOffFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component { class InvertFilter : public Filter { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; }; struct AutorepeatFilterTiming { @@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component { public: explicit AutorepeatFilter(std::vector timings); - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; @@ -102,7 +102,7 @@ class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; protected: std::function(bool)> f_; @@ -110,7 +110,7 @@ class LambdaFilter : public Filter { class SettleFilter : public Filter, public Component { public: - optional new_value(bool value) override; + optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; From f8681adec463433392be69e7ce20ff99b39294d6 Mon Sep 17 00:00:00 2001 From: Kent Gibson Date: Sun, 18 May 2025 11:45:12 +0800 Subject: [PATCH 140/193] Fix misspelling of climate in climate_ir.climate_ir_with_receiver_schema (#8829) --- esphome/components/ballu/climate.py | 2 +- esphome/components/climate_ir/__init__.py | 6 +++--- esphome/components/climate_ir_lg/climate.py | 2 +- esphome/components/coolix/climate.py | 2 +- esphome/components/daikin/climate.py | 2 +- esphome/components/daikin_arc/climate.py | 2 +- esphome/components/daikin_brc/climate.py | 2 +- esphome/components/delonghi/climate.py | 2 +- esphome/components/emmeti/climate.py | 2 +- esphome/components/fujitsu_general/climate.py | 2 +- esphome/components/gree/climate.py | 2 +- esphome/components/heatpumpir/climate.py | 2 +- esphome/components/hitachi_ac344/climate.py | 2 +- esphome/components/hitachi_ac424/climate.py | 2 +- esphome/components/midea_ir/climate.py | 2 +- esphome/components/mitsubishi/climate.py | 2 +- esphome/components/noblex/climate.py | 2 +- esphome/components/tcl112/climate.py | 2 +- esphome/components/toshiba/climate.py | 2 +- esphome/components/whirlpool/climate.py | 2 +- esphome/components/whynter/climate.py | 2 +- esphome/components/zhlt01/climate.py | 2 +- 22 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/ballu/climate.py b/esphome/components/ballu/climate.py index e35a1d244d..1127084632 100644 --- a/esphome/components/ballu/climate.py +++ b/esphome/components/ballu/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@bazuchan"] ballu_ns = cg.esphome_ns.namespace("ballu") BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate) async def to_code(config): diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 32b614e933..312b2ad900 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -40,7 +40,7 @@ def climate_ir_schema( ) -def climare_ir_with_receiver_schema( +def climate_ir_with_receiver_schema( class_: MockObjClass, ) -> cv.Schema: return climate_ir_schema(class_).extend( @@ -59,7 +59,7 @@ def deprecated_schema_constant(config): type = str(id.type).split("::", maxsplit=1)[0] _LOGGER.warning( "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " - "Please use `climate_ir.climare_ir_with_receiver_schema(...)` instead. " + "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. " "If you are seeing this, report an issue to the external_component author and ask them to update it. " "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " "Component using this schema: %s", @@ -68,7 +68,7 @@ def deprecated_schema_constant(config): return config -CLIMATE_IR_WITH_RECEIVER_SCHEMA = climare_ir_with_receiver_schema(ClimateIR) +CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR) CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py index de824bfe5c..9c832642ce 100644 --- a/esphome/components/climate_ir_lg/climate.py +++ b/esphome/components/climate_ir_lg/climate.py @@ -13,7 +13,7 @@ CONF_BIT_HIGH = "bit_high" CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ZERO_LOW = "bit_zero_low" -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(LgIrClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend( { cv.Optional( CONF_HEADER_HIGH, default="8000us" diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index b280544a5c..1ebcff3c1b 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"] coolix_ns = cg.esphome_ns.namespace("coolix") CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(CoolixClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate) async def to_code(config): diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py index 2cd44969c1..7f0226143b 100644 --- a/esphome/components/daikin/climate.py +++ b/esphome/components/daikin/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] daikin_ns = cg.esphome_ns.namespace("daikin") DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate) async def to_code(config): diff --git a/esphome/components/daikin_arc/climate.py b/esphome/components/daikin_arc/climate.py index 8f6b07315d..dbaf12d959 100644 --- a/esphome/components/daikin_arc/climate.py +++ b/esphome/components/daikin_arc/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinArcClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinArcClimate) async def to_code(config): diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py index 1000784380..5b7a4631a9 100644 --- a/esphome/components/daikin_brc/climate.py +++ b/esphome/components/daikin_brc/climate.py @@ -9,7 +9,7 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinBrcClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinBrcClimate).extend( { cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py index ff878b4ff7..63576f032d 100644 --- a/esphome/components/delonghi/climate.py +++ b/esphome/components/delonghi/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] delonghi_ns = cg.esphome_ns.namespace("delonghi") DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DelonghiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DelonghiClimate) async def to_code(config): diff --git a/esphome/components/emmeti/climate.py b/esphome/components/emmeti/climate.py index 042f1af64b..56e8e2b804 100644 --- a/esphome/components/emmeti/climate.py +++ b/esphome/components/emmeti/climate.py @@ -7,7 +7,7 @@ AUTO_LOAD = ["climate_ir"] emmeti_ns = cg.esphome_ns.namespace("emmeti") EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(EmmetiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(EmmetiClimate) async def to_code(config): diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py index 0f028d0af3..a104eafbcc 100644 --- a/esphome/components/fujitsu_general/climate.py +++ b/esphome/components/fujitsu_general/climate.py @@ -8,7 +8,7 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_( "FujitsuGeneralClimate", climate_ir.ClimateIR ) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(FujitsuGeneralClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FujitsuGeneralClimate) async def to_code(config): diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py index 947ef9bb97..057ba67b94 100644 --- a/esphome/components/gree/climate.py +++ b/esphome/components/gree/climate.py @@ -21,7 +21,7 @@ MODELS = { "yag": Model.GREE_YAG, } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(GreeClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend( { cv.Required(CONF_MODEL): cv.enum(MODELS), } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 21b0168615..c0eb8db4b3 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -97,7 +97,7 @@ VERTICAL_DIRECTIONS = { } CONFIG_SCHEMA = cv.All( - climate_ir.climare_ir_with_receiver_schema(HeatpumpIRClimate).extend( + climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend( { cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py index 4fa2d54fbb..ebdf4e8db4 100644 --- a/esphome/components/hitachi_ac344/climate.py +++ b/esphome/components/hitachi_ac344/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate) async def to_code(config): diff --git a/esphome/components/hitachi_ac424/climate.py b/esphome/components/hitachi_ac424/climate.py index 4b20147922..fde4e77545 100644 --- a/esphome/components/hitachi_ac424/climate.py +++ b/esphome/components/hitachi_ac424/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate) async def to_code(config): diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 5c9256b5e4..cbf5fae6fe 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -10,7 +10,7 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir") MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MideaIR).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend( { cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py index 5784d3ee8a..8291d70346 100644 --- a/esphome/components/mitsubishi/climate.py +++ b/esphome/components/mitsubishi/climate.py @@ -43,7 +43,7 @@ VERTICAL_DIRECTIONS = { } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MitsubishiClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend( { cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE), cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean, diff --git a/esphome/components/noblex/climate.py b/esphome/components/noblex/climate.py index d619265d01..19c4b6a08e 100644 --- a/esphome/components/noblex/climate.py +++ b/esphome/components/noblex/climate.py @@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"] noblex_ns = cg.esphome_ns.namespace("noblex") NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(NoblexClimate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(NoblexClimate) async def to_code(config): diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 9864113a52..58ed7ee529 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"] tcl112_ns = cg.esphome_ns.namespace("tcl112") Tcl112Climate = tcl112_ns.class_("Tcl112Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Tcl112Climate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(Tcl112Climate) async def to_code(config): diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 40112fc460..b8e390dd66 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -16,7 +16,7 @@ MODELS = { "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ToshibaClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend( { cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True), } diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py index daee9e7fb7..f969a505fb 100644 --- a/esphome/components/whirlpool/climate.py +++ b/esphome/components/whirlpool/climate.py @@ -15,7 +15,7 @@ MODELS = { "DG11J1-91": Model.MODEL_DG11J1_91, } -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(WhirlpoolClimate).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend( { cv.Optional(CONF_MODEL, default="DG11J1-3A"): cv.enum(MODELS, upper=True), } diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py index 4a01c014c7..bf33890d9c 100644 --- a/esphome/components/whynter/climate.py +++ b/esphome/components/whynter/climate.py @@ -9,7 +9,7 @@ whynter_ns = cg.esphome_ns.namespace("whynter") Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Whynter).extend( +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(Whynter).extend( { cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, } diff --git a/esphome/components/zhlt01/climate.py b/esphome/components/zhlt01/climate.py index d5098ab42c..8d0c50308b 100644 --- a/esphome/components/zhlt01/climate.py +++ b/esphome/components/zhlt01/climate.py @@ -7,7 +7,7 @@ CODEOWNERS = ["@cfeenstra1024"] zhlt01_ns = cg.esphome_ns.namespace("zhlt01") ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ZHLT01Climate) +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ZHLT01Climate) async def to_code(config): From 316fe2f06cfa7333b072e9ef09c2b8d5d18f3bd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 15:43:41 -0400 Subject: [PATCH 141/193] Fix ESP32 console logging corruption and message loss in multi-task (#8806) --- esphome/components/logger/logger.cpp | 44 +++++++++++++++++++--------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 0ad909cb07..014f7e3dec 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -16,9 +16,14 @@ static const char *const TAG = "logger"; #ifdef USE_ESP32 // Implementation for ESP32 (multi-task platform with task-specific tracking) // Main task always uses direct buffer access for console output and callbacks -// Other tasks: -// - With task log buffer: stack buffer for console output, async buffer for callbacks -// - Without task log buffer: only console output, no callbacks +// +// For non-main tasks: +// - WITH task log buffer: Prefer sending to ring buffer for async processing +// - Avoids allocating stack memory for console output in normal operation +// - Prevents console corruption from concurrent writes by multiple tasks +// - Messages are serialized through main loop for proper console output +// - Fallback to emergency console logging only if ring buffer is full +// - WITHOUT task log buffer: Only emergency console output, no callbacks void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag)) return; @@ -38,8 +43,18 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * return; } - // For non-main tasks: use stack-allocated buffer only for console output - if (this->baud_rate_ > 0) { // If logging is enabled, write to console + bool message_sent = false; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered + message_sent = this->log_buffer_->send_message_thread_safe(static_cast(level), tag, + static_cast(line), current_task, format, args); +#endif // USE_ESPHOME_TASK_LOG_BUFFER + + // Emergency console logging for non-main tasks when ring buffer is full or disabled + // This is a fallback mechanism to ensure critical log messages are visible + // Note: This may cause interleaved/corrupted console output if multiple tasks + // log simultaneously, but it's better than losing important messages entirely + if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety @@ -49,15 +64,6 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * this->write_msg_(console_buffer); } -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered - if (this->log_callback_.size() > 0) { - // This will be processed in the main loop - this->log_buffer_->send_message_thread_safe(static_cast(level), tag, static_cast(line), - current_task, format, args); - } -#endif // USE_ESPHOME_TASK_LOG_BUFFER - // Reset the recursion guard for this task this->reset_task_log_recursion_(is_main_task); } @@ -184,7 +190,17 @@ void Logger::loop() { this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); + // At this point all the data we need from message has been transferred to the tx_buffer + // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); + + // Write to console from the main loop to prevent corruption from concurrent writes + // This ensures all log messages appear on the console in a clean, serialized manner + // Note: Messages may appear slightly out of order due to async processing, but + // this is preferred over corrupted/interleaved console output + if (this->baud_rate_ > 0) { + this->write_msg_(this->tx_buffer_); + } } } #endif From 15d0b4355ebf88699d423b95e0226100854f02b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 15:48:57 -0400 Subject: [PATCH 142/193] Reduce number of calls to fetch time in the main loop (#8804) --- esphome/components/api/api_connection.cpp | 5 +-- esphome/components/bedjet/bedjet_hub.cpp | 1 + .../bluetooth_proxy/bluetooth_proxy.cpp | 3 +- esphome/components/cse7766/cse7766.cpp | 3 +- .../current_based/current_based_cover.cpp | 3 +- esphome/components/daly_bms/daly_bms.cpp | 3 +- esphome/components/debug/debug_component.cpp | 2 +- esphome/components/endstop/endstop_cover.cpp | 3 +- .../components/esp32_ble/ble_advertising.cpp | 3 +- .../components/esp32_camera/esp32_camera.cpp | 3 +- .../esp32_improv/esp32_improv_component.cpp | 2 +- .../components/esp32_touch/esp32_touch.cpp | 2 +- .../ethernet/ethernet_component.cpp | 2 +- .../components/feedback/feedback_cover.cpp | 3 +- esphome/components/gcja5/gcja5.cpp | 3 +- .../growatt_solar/growatt_solar.cpp | 3 +- esphome/components/kuntze/kuntze.cpp | 3 +- .../matrix_keypad/matrix_keypad.cpp | 3 +- .../components/max7219digit/max7219digit.cpp | 3 +- esphome/components/modbus/modbus.cpp | 3 +- esphome/components/mqtt/mqtt_client.cpp | 2 +- esphome/components/pmsx003/pmsx003.cpp | 3 +- esphome/components/pzem004t/pzem004t.cpp | 3 +- esphome/components/rf_bridge/rf_bridge.cpp | 3 +- esphome/components/sds011/sds011.cpp | 3 +- .../components/slow_pwm/slow_pwm_output.cpp | 3 +- esphome/components/sprinkler/sprinkler.cpp | 11 ++++--- .../time_based/time_based_cover.cpp | 3 +- .../components/uart/switch/uart_switch.cpp | 3 +- .../climate/uponor_smatrix_climate.cpp | 3 +- .../uponor_smatrix/uponor_smatrix.cpp | 3 +- esphome/core/application.cpp | 32 +++++++++++++------ esphome/core/application.h | 6 +++- esphome/core/component.cpp | 15 ++++++--- esphome/core/component.h | 6 +++- esphome/core/scheduler.cpp | 5 ++- 36 files changed, 107 insertions(+), 53 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ee0451f499..847d7840dc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -8,6 +8,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/version.h" +#include "esphome/core/application.h" #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -146,7 +147,7 @@ void APIConnection::loop() { } return; } else { - this->last_traffic_ = millis(); + this->last_traffic_ = App.get_loop_component_start_time(); // read a packet this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); if (this->remove_) @@ -163,7 +164,7 @@ void APIConnection::loop() { static uint32_t keepalive = 60000; static uint8_t max_ping_retries = 60; static uint16_t ping_retry_interval = 1000; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (keepalive * 5) / 2) { diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 6404298697..fea7080de6 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -3,6 +3,7 @@ #include "bedjet_hub.h" #include "bedjet_child.h" #include "bedjet_const.h" +#include "esphome/core/application.h" #include namespace esphome { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 915d2882d3..d8b2111cb0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" +#include "esphome/core/application.h" #ifdef USE_ESP32 @@ -177,7 +178,7 @@ void BluetoothProxy::loop() { // Flush any pending BLE advertisements that have been accumulated but not yet sent if (this->raw_advertisements_) { static uint32_t last_flush_time = 0; - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); // Flush accumulated advertisements every 100ms if (now - last_flush_time >= 100) { diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 88a91e374a..b0876778a3 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,5 +1,6 @@ #include "cse7766.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace cse7766 { @@ -7,7 +8,7 @@ namespace cse7766 { static const char *const TAG = "cse7766"; void CSE7766Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_transmission_ >= 500) { // last transmission too long ago. Reset RX index. this->raw_data_index_ = 0; diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 8404e07894..8bb27dbeca 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -1,6 +1,7 @@ #include "current_based_cover.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include namespace esphome { @@ -60,7 +61,7 @@ void CurrentBasedCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->current_operation == COVER_OPERATION_OPENING) { if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 929f31e008..1dd0520465 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -1,6 +1,7 @@ #include "daly_bms.h" #include #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace daly_bms { @@ -32,7 +33,7 @@ void DalyBmsComponent::update() { } void DalyBmsComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->receiving_ && (now - this->last_transmission_ >= 200)) { // last transmission too long ago. Reset RX index. ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index."); diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 5bcc676247..c4de42c7e9 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -70,7 +70,7 @@ void DebugComponent::loop() { #ifdef USE_SENSOR // calculate loop time - from last call to this one if (this->loop_time_sensor_ != nullptr) { - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); uint32_t loop_time = now - this->last_loop_timetag_; this->max_loop_time_ = std::max(this->max_loop_time_, loop_time); this->last_loop_timetag_ = now; diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 1190acc46b..381f098eb5 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -1,6 +1,7 @@ #include "endstop_cover.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" namespace esphome { namespace endstop { @@ -65,7 +66,7 @@ void EndstopCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) { float dur = (now - this->start_dir_time_) / 1e3f; diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 1d340c76d9..8d43b5af33 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -6,6 +6,7 @@ #include #include "ble_uuid.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace esp32_ble { @@ -143,7 +144,7 @@ void BLEAdvertising::loop() { if (this->raw_advertisements_callbacks_.empty()) { return; } - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { this->stop(); this->current_adv_index_ += 1; diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index e9e9d3cffb..60accba747 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -3,6 +3,7 @@ #include "esp32_camera.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include @@ -162,7 +163,7 @@ void ESP32Camera::loop() { } // request idle image every idle_update_interval - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { this->last_idle_request_ = now; this->request_image(IDLE); diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index b720425506..d74714838f 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -92,7 +92,7 @@ void ESP32ImprovComponent::loop() { if (!this->incoming_data_.empty()) this->process_incoming_data_(); - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case improv::STATE_STOPPED: diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 69e4e37cec..1dcb39e5de 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -288,7 +288,7 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { } void ESP32TouchComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { child->value_ = this->component_touch_pad_read(child->get_touch_pad()); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 68a6e617fc..79ef0715d8 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -240,7 +240,7 @@ void EthernetComponent::setup() { } void EthernetComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case EthernetComponentState::STOPPED: diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index fa3166ba65..e419ee6229 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -1,6 +1,7 @@ #include "feedback_cover.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace feedback { @@ -220,7 +221,7 @@ void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_o void FeedbackCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Recompute position every loop cycle this->recompute_position_(); diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index b1db58654b..64b237515b 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -6,6 +6,7 @@ */ #include "gcja5.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include namespace esphome { @@ -16,7 +17,7 @@ static const char *const TAG = "gcja5"; void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } void GCJA5Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_transmission_ >= 500) { // last transmission too long ago. Reset RX index. this->rx_message_.clear(); diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index c4ed5ab841..60fd1379e8 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -1,5 +1,6 @@ #include "growatt_solar.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace growatt_solar { @@ -18,7 +19,7 @@ void GrowattSolar::loop() { void GrowattSolar::update() { // If our last send has had no reply yet, and it wasn't that long ago, do nothing. - uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_send_ < this->get_update_interval() / 2) { return; } diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index e50dafca86..8ab7af8cd9 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -1,5 +1,6 @@ #include "kuntze.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace kuntze { @@ -60,7 +61,7 @@ void Kuntze::on_modbus_data(const std::vector &data) { } void Kuntze::loop() { - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); // timeout after 15 seconds if (this->waiting_ && (now - this->last_send_ > 15000)) { ESP_LOGW(TAG, "timed out waiting for response"); diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 8537997935..6cb4fc4f3c 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -1,5 +1,6 @@ #include "matrix_keypad.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace matrix_keypad { @@ -28,7 +29,7 @@ void MatrixKeypad::setup() { void MatrixKeypad::loop() { static uint32_t active_start = 0; static int active_key = -1; - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); int key = -1; bool error = false; int pos = 0, row, col; diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 13b75ca734..154accd66f 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "max7219font.h" #include @@ -63,7 +64,7 @@ void MAX7219Component::dump_config() { } void MAX7219Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); const uint32_t millis_since_last_scroll = now - this->last_scroll_; const size_t first_line_size = this->max_displaybuffer_[0].size(); // check if the buffer has shrunk past the current position since last update diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 47deea83e6..80c2ffe3d6 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -1,6 +1,7 @@ #include "modbus.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" namespace esphome { namespace modbus { @@ -13,7 +14,7 @@ void Modbus::setup() { } } void Modbus::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); while (this->available()) { uint8_t byte; diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1fcef3293c..e3722099a7 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -345,7 +345,7 @@ void MQTTClientComponent::loop() { this->disconnect_reason_.reset(); } - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case MQTT_CLIENT_DISABLED: diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 11626768d8..0abed8a5a4 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -1,5 +1,6 @@ #include "pmsx003.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace pmsx003 { @@ -42,7 +43,7 @@ void PMSX003Component::dump_config() { } void PMSX003Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // If we update less often than it takes the device to stabilise, spin the fan down // rather than running it constantly. It does take some time to stabilise, so we diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index 35b66b03f2..356847825e 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -1,5 +1,6 @@ #include "pzem004t.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include namespace esphome { @@ -16,7 +17,7 @@ void PZEM004T::setup() { } void PZEM004T::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_read_ > 500 && this->available() < 7) { while (this->available()) this->read(); diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 3b3e00a416..52ce037dbe 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -1,5 +1,6 @@ #include "rf_bridge.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include #include @@ -128,7 +129,7 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) { } void RFBridgeComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_bridge_byte_ > 50) { this->rx_buffer_.clear(); this->last_bridge_byte_ = now; diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 0c04ff557f..a34059d85d 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -1,5 +1,6 @@ #include "sds011.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace sds011 { @@ -75,7 +76,7 @@ void SDS011Component::dump_config() { } void SDS011Component::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if ((now - this->last_transmission_ >= 500) && this->data_index_) { // last transmission too long ago. Reset RX index. ESP_LOGV(TAG, "Last transmission too long ago. Reset RX index."); diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index d6b2cdfe12..643294303c 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -1,5 +1,6 @@ #include "slow_pwm_output.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace slow_pwm { @@ -39,7 +40,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) { } void SlowPWMOutput::loop() { - uint32_t now = millis(); + uint32_t now = App.get_loop_component_start_time(); float scaled_state = this->state_ * this->period_; if (now - this->period_start_time_ >= this->period_) { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 3cfb5ccdee..50ea3eff51 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -20,7 +20,7 @@ SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *o bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } void SprinklerSwitch::loop() { - if ((this->pinned_millis_) && (millis() > this->pinned_millis_ + this->pulse_duration_)) { + if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) { this->pinned_millis_ = 0; // reset tracker if (this->off_switch_->state) { this->off_switch_->turn_off(); @@ -148,22 +148,23 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler : controller_(controller), valve_(valve) {} void SprinklerValveOperator::loop() { - if (millis() >= this->start_millis_) { // dummy check + uint32_t now = App.get_loop_component_start_time(); + if (now >= this->start_millis_) { // dummy check switch (this->state_) { case STARTING: - if (millis() > (this->start_millis_ + this->start_delay_)) { + if (now > (this->start_millis_ + this->start_delay_)) { this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state } break; case ACTIVE: - if (millis() > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { + if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down } break; case STOPPING: - if (millis() > (this->stop_millis_ + this->stop_delay_)) { + if (now > (this->stop_millis_ + this->stop_delay_)) { this->kill_(); // stop_delay_has been exceeded, ensure all valves are off } break; diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index e1936d5ee1..ec219d6db7 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -1,6 +1,7 @@ #include "time_based_cover.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" namespace esphome { namespace time_based { @@ -26,7 +27,7 @@ void TimeBasedCover::loop() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Recompute position every loop cycle this->recompute_position_(); diff --git a/esphome/components/uart/switch/uart_switch.cpp b/esphome/components/uart/switch/uart_switch.cpp index 1edb54641b..96f50ff50f 100644 --- a/esphome/components/uart/switch/uart_switch.cpp +++ b/esphome/components/uart/switch/uart_switch.cpp @@ -1,5 +1,6 @@ #include "uart_switch.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace uart { @@ -8,7 +9,7 @@ static const char *const TAG = "uart.switch"; void UARTSwitch::loop() { if (this->send_every_) { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_transmission_ > this->send_every_) { this->write_command_(this->state); this->last_transmission_ = now; diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 5afc628db3..cc9b8a0f90 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -1,6 +1,7 @@ #include "uponor_smatrix_climate.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace uponor_smatrix { @@ -13,7 +14,7 @@ void UponorSmatrixClimate::dump_config() { } void UponorSmatrixClimate::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Publish state after all update packets are processed if (this->last_data_ != 0 && (now - this->last_data_ > 100) && this->target_temperature_raw_ != 0) { diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index e058de2852..2dbbef72ab 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -1,5 +1,6 @@ #include "uponor_smatrix.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace uponor_smatrix { @@ -35,7 +36,7 @@ void UponorSmatrixComponent::dump_config() { } void UponorSmatrixComponent::loop() { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); // Discard stale data if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) { diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3f5a283fd8..a81a2c580c 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -67,22 +67,32 @@ void Application::loop() { uint32_t new_app_state = 0; this->scheduler.call(); - this->feed_wdt(); + + // Get the initial loop time at the start + uint32_t last_op_end_time = millis(); + + // Feed WDT with time + this->feed_wdt(last_op_end_time); + for (Component *component : this->looping_components_) { + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + { this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component}; + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; component->call(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); } new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; - this->feed_wdt(); + this->feed_wdt(last_op_end_time); } this->app_state_ = new_app_state; - const uint32_t now = millis(); - - auto elapsed = now - this->last_loop_; + // Use the last component's end time instead of calling millis() again + auto elapsed = last_op_end_time - this->last_loop_; if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { yield(); } else { @@ -94,7 +104,7 @@ void Application::loop() { delay_time = std::min(next_schedule, delay_time); delay(delay_time); } - this->last_loop_ = now; + this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ == 0) { @@ -109,10 +119,12 @@ void Application::loop() { } } -void IRAM_ATTR HOT Application::feed_wdt() { +void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { static uint32_t last_feed = 0; - uint32_t now = micros(); - if (now - last_feed > 3000) { + // Use provided time if available, otherwise get current time + uint32_t now = time ? time : millis(); + // Compare in milliseconds (3ms threshold) + if (now - last_feed > 3) { arch_feed_wdt(); last_feed = now; #ifdef USE_STATUS_LED diff --git a/esphome/core/application.h b/esphome/core/application.h index e64e2b7655..aa44d9ba1d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -217,6 +217,9 @@ class Application { std::string get_compilation_time() const { return this->compilation_time_; } + /// Get the cached time in milliseconds from when the current component started its loop execution + inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; } + /** Set the target interval with which to run the loop() calls. * If the loop() method takes longer than the target interval, ESPHome won't * sleep in loop(), but if the time spent in loop() is small than the target, ESPHome @@ -236,7 +239,7 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } - void feed_wdt(); + void feed_wdt(uint32_t time = 0); void reboot(); @@ -551,6 +554,7 @@ class Application { size_t dump_config_at_{SIZE_MAX}; uint32_t app_state_{0}; Component *current_component_{nullptr}; + uint32_t loop_component_start_time_{0}; }; /// Global storage of Application pointer - only one Application can exist. diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a7e451b93d..1141e4067d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -240,10 +240,12 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } -WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) - : started_(millis()), component_(component) {} -WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { - uint32_t blocking_time = millis() - this->started_; +WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) + : started_(start_time), component_(component) {} +uint32_t WarnIfComponentBlockingGuard::finish() { + uint32_t curr_time = millis(); + + uint32_t blocking_time = curr_time - this->started_; bool should_warn; if (this->component_ != nullptr) { should_warn = this->component_->should_warn_of_blocking(blocking_time); @@ -254,8 +256,11 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { const char *src = component_ == nullptr ? "" : component_->get_component_source(); ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms."); - ; } + + return curr_time; } +WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 412074282d..7b3e12eb59 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -339,7 +339,11 @@ class PollingComponent : public Component { class WarnIfComponentBlockingGuard { public: - WarnIfComponentBlockingGuard(Component *component); + WarnIfComponentBlockingGuard(Component *component, uint32_t start_time); + + // Finish the timing operation and return the current time + uint32_t finish(); + ~WarnIfComponentBlockingGuard(); protected: diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b4f617d405..2dea450ead 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -229,8 +229,11 @@ void HOT Scheduler::call() { // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled { - WarnIfComponentBlockingGuard guard{item->component}; + uint32_t now_ms = millis(); + WarnIfComponentBlockingGuard guard{item->component, now_ms}; item->callback(); + // Call finish to ensure blocking time is properly calculated and reported + guard.finish(); } } From 4d54cb9b316bf35b32d26332a2887ceae26fa30a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 17:05:20 -0400 Subject: [PATCH 143/193] Refactor API frame helpers to enable buffer reuse (#8825) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 9 +- esphome/components/api/api_frame_helper.cpp | 128 ++++++++++++++------ esphome/components/api/api_frame_helper.h | 41 ++++++- esphome/components/api/proto.h | 28 +++++ 5 files changed, 162 insertions(+), 46 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 847d7840dc..d71e5587a3 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1962,7 +1962,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } } - APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); + APIError err = this->helper_->write_protobuf_packet(message_type, buffer); if (err == APIError::WOULD_BLOCK) return false; if (err != APIError::OK) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 1e47418d90..b40e9602be 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -315,7 +315,14 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { // FIXME: ensure no recursive writes can happen this->proto_write_buffer_.clear(); - this->proto_write_buffer_.reserve(reserve_size); + // Get header padding size - used for both reserve and insert + uint8_t header_padding = this->helper_->frame_header_padding(); + // Reserve space for header padding + message + footer + // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) + // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) + this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); + // Insert header padding bytes so message encoding starts at the correct position + this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0); return {&this->proto_write_buffer_}; } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index f251ceb6e4..f18f4104b6 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -493,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea std::vector data; data.resize(reason.length() + 1); data[0] = 0x01; // failure - for (size_t i = 0; i < reason.length(); i++) { - data[i + 1] = (uint8_t) reason[i]; + + // Copy error message in bulk + if (!reason.empty()) { + std::memcpy(data.data() + 1, reason.c_str(), reason.length()); } + // temporarily remove failed state auto orig_state = state_; state_ = State::EXPLICIT_REJECT; @@ -557,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } -APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { +APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { int err; APIError aerr; aerr = state_action_(); @@ -569,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload return APIError::WOULD_BLOCK; } + std::vector *raw_buffer = buffer.get_buffer(); + // Message data starts after padding + size_t payload_len = raw_buffer->size() - frame_header_padding_; size_t padding = 0; size_t msg_len = 4 + payload_len + padding; - size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); - auto tmpbuf = std::unique_ptr{new (std::nothrow) uint8_t[frame_len]}; - if (tmpbuf == nullptr) { - HELPER_LOG("Could not allocate for writing packet"); - return APIError::OUT_OF_MEMORY; - } - tmpbuf[0] = 0x01; // indicator - // tmpbuf[1], tmpbuf[2] to be set later + // We need to resize to include MAC space, but we already reserved it in create_buffer + raw_buffer->resize(raw_buffer->size() + frame_footer_size_); + + // Write the noise header in the padded area + // Buffer layout: + // [0] - 0x01 indicator byte + // [1-2] - Size of encrypted payload (filled after encryption) + // [3-4] - Message type (encrypted) + // [5-6] - Payload length (encrypted) + // [7...] - Actual payload data (encrypted) + uint8_t *buf_start = raw_buffer->data(); + buf_start[0] = 0x01; // indicator + // buf_start[1], buf_start[2] to be set later after encryption const uint8_t msg_offset = 3; - const uint8_t payload_offset = msg_offset + 4; - tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type - tmpbuf[msg_offset + 1] = (uint8_t) type; - tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len - tmpbuf[msg_offset + 3] = (uint8_t) payload_len; - // copy data - std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); - // fill padding with zeros - std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); + buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte + buf_start[msg_offset + 1] = (uint8_t) type; // type low byte + buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte + buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte + // payload data is already in the buffer starting at position 7 NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); + // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption + noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_); err = noise_cipherstate_encrypt(send_cipher_, &mbuf); if (err != 0) { state_ = State::FAILED; @@ -602,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload } size_t total_len = 3 + mbuf.size; - tmpbuf[1] = (uint8_t) (mbuf.size >> 8); - tmpbuf[2] = (uint8_t) mbuf.size; + buf_start[1] = (uint8_t) (mbuf.size >> 8); + buf_start[2] = (uint8_t) mbuf.size; struct iovec iov; - iov.iov_base = &tmpbuf[0]; + // Point iov_base to the beginning of the buffer (no unused padding in Noise) + // We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC) + iov.iov_base = buf_start; iov.iov_len = total_len; // write raw to not have two packets sent if NAGLE disabled @@ -718,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { return APIError::HANDSHAKESTATE_SPLIT_FAILED; } + frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); + HELPER_LOG("Handshake complete!"); noise_handshakestate_free(handshake_); handshake_ = nullptr; @@ -990,28 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } -APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { +APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { if (state_ != State::DATA) { return APIError::BAD_STATE; } - std::vector header; - header.reserve(1 + api::ProtoSize::varint(static_cast(payload_len)) + - api::ProtoSize::varint(static_cast(type))); - header.push_back(0x00); - ProtoVarInt(payload_len).encode(header); - ProtoVarInt(type).encode(header); + std::vector *raw_buffer = buffer.get_buffer(); + // Message data starts after padding (frame_header_padding_ = 6) + size_t payload_len = raw_buffer->size() - frame_header_padding_; - struct iovec iov[2]; - iov[0].iov_base = &header[0]; - iov[0].iov_len = header.size(); - if (payload_len == 0) { - return write_raw_(iov, 1); + // Calculate varint sizes for header components + size_t size_varint_len = api::ProtoSize::varint(static_cast(payload_len)); + size_t type_varint_len = api::ProtoSize::varint(static_cast(type)); + size_t total_header_len = 1 + size_varint_len + type_varint_len; + + if (total_header_len > frame_header_padding_) { + // Header is too large to fit in the padding + return APIError::BAD_ARG; } - iov[1].iov_base = const_cast(payload); - iov[1].iov_len = payload_len; - return write_raw_(iov, 2); + // Calculate where to start writing the header + // The header starts at the latest possible position to minimize unused padding + // + // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 + // [0-2] - Unused padding + // [3] - 0x00 indicator byte + // [4] - Payload size varint (1 byte, for sizes 0-127) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 + // [0-1] - Unused padding + // [2] - 0x00 indicator byte + // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 + // [0] - 0x00 indicator byte + // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) + // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [6...] - Actual payload data + uint8_t *buf_start = raw_buffer->data(); + size_t header_offset = frame_header_padding_ - total_header_len; + + // Write the plaintext header + buf_start[header_offset] = 0x00; // indicator + + // Encode size varint directly into buffer + ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + + // Encode type varint directly into buffer + ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + + struct iovec iov; + // Point iov_base to the beginning of our header (skip unused padding) + // This ensures we only send the actual header and payload, not the empty padding bytes + iov.iov_base = buf_start + header_offset; + iov.iov_len = total_header_len + payload_len; + + return write_raw_(&iov, 1); } APIError APIPlaintextFrameHelper::try_send_tx_buf_() { // try send from tx_buf diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index db506ea1ce..25bfd594ec 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -16,6 +16,8 @@ namespace esphome { namespace api { +class ProtoWriteBuffer; + struct ReadPacketBuffer { std::vector container; uint16_t type; @@ -65,32 +67,46 @@ class APIFrameHelper { virtual APIError loop() = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual bool can_write_without_blocking() = 0; - virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; + virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0; virtual std::string getpeername() = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; virtual APIError close() = 0; virtual APIError shutdown(int how) = 0; // Give this helper a name for logging virtual void set_log_info(std::string info) = 0; + // Get the frame header padding required by this protocol + virtual uint8_t frame_header_padding() = 0; + // Get the frame footer size required by this protocol + virtual uint8_t frame_footer_size() = 0; protected: // Common implementation for writing raw data to socket template APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); + + uint8_t frame_header_padding_{0}; + uint8_t frame_footer_size_{0}; }; #ifdef USE_API_NOISE class APINoiseFrameHelper : public APIFrameHelper { public: APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) - : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {} + : socket_(std::move(socket)), ctx_(std::move(ctx)) { + // Noise header structure: + // Pos 0: indicator (0x01) + // Pos 1-2: encrypted payload size (16-bit big-endian) + // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) + // Pos 7+: actual payload data + frame_header_padding_ = 7; + } ~APINoiseFrameHelper() override; APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; bool can_write_without_blocking() override; - APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; std::string getpeername() override { return this->socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return this->socket_->getpeername(addr, addrlen); @@ -99,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError shutdown(int how) override; // Give this helper a name for logging void set_log_info(std::string info) override { info_ = std::move(info); } + // Get the frame header padding required by this protocol + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } protected: struct ParsedFrame { @@ -152,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper { #ifdef USE_API_PLAINTEXT class APIPlaintextFrameHelper : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) { + // Plaintext header structure (worst case): + // Pos 0: indicator (0x00) + // Pos 1-3: payload size varint (up to 3 bytes) + // Pos 4-5: message type varint (up to 2 bytes) + // Pos 6+: actual payload data + frame_header_padding_ = 6; + } ~APIPlaintextFrameHelper() override = default; APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; bool can_write_without_blocking() override; - APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; std::string getpeername() override { return this->socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return this->socket_->getpeername(addr, addrlen); @@ -167,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError shutdown(int how) override; // Give this helper a name for logging void set_log_info(std::string info) override { info_ = std::move(info); } + // Get the frame header padding required by this protocol + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } protected: struct ParsedFrame { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e110a58eda..65bef0b6f7 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -83,6 +83,34 @@ class ProtoVarInt { return static_cast(this->value_ >> 1); } } + /** + * Encode the varint value to a pre-allocated buffer without bounds checking. + * + * @param buffer The pre-allocated buffer to write the encoded varint to + * @param len The size of the buffer in bytes + * + * @note The caller is responsible for ensuring the buffer is large enough + * to hold the encoded value. Use ProtoSize::varint() to calculate + * the exact size needed before calling this method. + * @note No bounds checking is performed for performance reasons. + */ + void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) { + uint64_t val = this->value_; + if (val <= 0x7F) { + buffer[0] = val; + return; + } + size_t i = 0; + while (val && i < len) { + uint8_t temp = val & 0x7F; + val >>= 7; + if (val) { + buffer[i++] = temp | 0x80; + } else { + buffer[i++] = temp; + } + } + } void encode(std::vector &out) { uint64_t val = this->value_; if (val <= 0x7F) { From 1b9ae57b9d553e251fdc6e1180a072cf0c64ac81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 09:20:29 +1200 Subject: [PATCH 144/193] Bump docker/build-push-action from 6.16.0 to 6.17.0 in /.github/actions/build-image (#8810) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 3d6de54f42..0e41314ce6 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.17.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.17.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 38cfd323829552e2aa7843894295c5a6c04dfdab Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 May 2025 09:24:53 +1200 Subject: [PATCH 145/193] Bump version to 2025.5.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f48de581ea..9b38c8b085 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0b2" +__version__ = "2025.5.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From dbce54477a2b6238510d09776482eb688f61c79f Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Sun, 18 May 2025 23:44:33 +0200 Subject: [PATCH 146/193] unify and add missing metric suffixes (#8816) --- esphome/config_validation.py | 43 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 2eabcc8568..54056240ce 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -982,23 +982,32 @@ def uuid(value): METRIC_SUFFIXES = { - "E": 1e18, - "P": 1e15, - "T": 1e12, - "G": 1e9, - "M": 1e6, - "k": 1e3, - "da": 10, - "d": 1e-1, - "c": 1e-2, - "m": 0.001, - "µ": 1e-6, - "u": 1e-6, - "n": 1e-9, - "p": 1e-12, - "f": 1e-15, - "a": 1e-18, - "": 1, + "Q": 1e30, # Quetta + "R": 1e27, # Ronna + "Y": 1e24, # Yotta + "Z": 1e21, # Zetta + "E": 1e18, # Exa + "P": 1e15, # Peta + "T": 1e12, # Tera + "G": 1e9, # Giga + "M": 1e6, # Mega + "k": 1e3, # Kilo + "h": 1e2, # Hecto + "da": 1e1, # Deca + "": 1e0, # No prefix + "d": 1e-1, # Deci + "c": 1e-2, # Centi + "m": 1e-3, # Milli + "µ": 1e-6, # Micro + "u": 1e-6, # Micro (same as µ) + "n": 1e-9, # Nano + "p": 1e-12, # Pico + "f": 1e-15, # Femto + "a": 1e-18, # Atto + "z": 1e-21, # Zepto + "y": 1e-24, # Yocto + "r": 1e-27, # Ronto + "q": 1e-30, # Quecto } From 87a9dd18c8213c95c5c76aba913934f1b11b661e Mon Sep 17 00:00:00 2001 From: DanielV Date: Mon, 19 May 2025 00:01:30 +0200 Subject: [PATCH 147/193] Improve stability for a test that crashes intermittently in CI (#8699) Co-authored-by: NP v/d Spek --- tests/dashboard/test_web_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index a61850abf3..1ea9c73f32 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -75,6 +75,7 @@ async def test_devices_page(dashboard: DashboardTestHelper) -> None: assert response.headers["content-type"] == "application/json" json_data = json.loads(response.body.decode()) configured_devices = json_data["configured"] + assert len(configured_devices) != 0 first_device = configured_devices[0] assert first_device["name"] == "pico" assert first_device["configuration"] == "pico.yaml" From 99c4f88c3f3ac43575638340ef267518173ccf8c Mon Sep 17 00:00:00 2001 From: Fexiven <48439988+Fexiven@users.noreply.github.com> Date: Mon, 19 May 2025 01:01:31 +0200 Subject: [PATCH 148/193] Update esp32-camera library version (#8832) --- esphome/components/esp32_camera/__init__.py | 2 +- esphome/components/esp32_camera/esp32_camera.cpp | 9 --------- esphome/idf_component.yml | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index e55c54f097..b4038c1841 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -296,7 +296,7 @@ async def to_code(config): add_idf_component( name="esp32-camera", repo="https://github.com/espressif/esp32-camera.git", - ref="v2.0.9", + ref="v2.0.15", ) for conf in config.get(CONF_ON_STREAM_START, []): diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 60accba747..cfcf7869d4 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -55,11 +55,7 @@ void ESP32Camera::dump_config() { ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href); ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk); ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz); -#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated - ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl); -#else ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl); -#endif ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset); switch (this->config_.frame_size) { case FRAMESIZE_QQVGA: @@ -239,13 +235,8 @@ void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) { this->config_.xclk_freq_hz = frequency; } void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { -#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated - this->config_.pin_sscb_sda = sda; - this->config_.pin_sscb_scl = scl; -#else this->config_.pin_sccb_sda = sda; this->config_.pin_sccb_scl = scl; -#endif } void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c273cae07e..8460de5638 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: version: v1.3.1 esp32_camera: git: https://github.com/espressif/esp32-camera.git - version: v2.0.9 + version: v2.0.15 mdns: git: https://github.com/espressif/esp-protocols.git version: mdns-v1.8.2 From 904495e1b87f3566361ee91911e959bff2503396 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 May 2025 13:57:41 +1200 Subject: [PATCH 149/193] Fix api doc homepage (#8836) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/weikai/weikai.cpp | 52 ---------------------------- esphome/core/doxygen.h | 13 +++++++ 2 files changed, 13 insertions(+), 52 deletions(-) create mode 100644 esphome/core/doxygen.h diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 00bce9bcff..19aa09e20d 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -8,58 +8,6 @@ namespace esphome { namespace weikai { -/*! @mainpage Weikai source code documentation - This documentation provides information about the implementation of the family of WeiKai Components in ESPHome. - Here is the class diagram related to Weikai family of components: - @image html weikai_class.png - - @section WKRingBuffer_ The WKRingBuffer template class -The WKRingBuffer template class has it names implies implement a simple ring buffer helper class. This straightforward -container implements FIFO functionality, enabling bytes to be pushed into one side and popped from the other in the -order of entry. Implementation is classic and therefore not described in any details. - - @section WeikaiRegister_ The WeikaiRegister class - The WeikaiRegister helper class creates objects that act as proxies to the device registers. - @details This is an abstract virtual class (interface) that provides all the necessary access to registers while hiding - the actual implementation. The access to the registers can be made through an I²C bus in for example for wk2168_i2c - component or through a SPI bus for example in the case of the wk2168_spi component. Derived classes will actually - performs the specific bus operations. - - @section WeikaiRegisterI2C_ WeikaiRegisterI2C - The weikai_i2c::WeikaiRegisterI2C class implements the virtual methods of the WeikaiRegister class for an I2C bus. - - @section WeikaiRegisterSPI_ WeikaiRegisterSPI - The weikai_spi::WeikaiRegisterSPI class implements the virtual methods of the WeikaiRegister class for an SPI bus. - - @section WeikaiComponent_ The WeikaiComponent class -The WeikaiComponent class stores the information global to a WeiKai family component and provides methods to set/access -this information. It also serves as a container for WeikaiChannel instances. This is done by maintaining an array of -references these WeikaiChannel instances. This class derives from the esphome::Component classes. This class override -esphome::Component::loop() method to facilitate the seamless transfer of accumulated bytes from the receive -FIFO into the ring buffer. This process ensures quick access to the stored bytes, enhancing the overall efficiency of -the component. - - @section WeikaiComponentI2C_ WeikaiComponentI2C - The weikai_i2c::WeikaiComponentI2C class implements the virtual methods of the WeikaiComponent class for an I2C bus. - - @section WeikaiComponentSPI_ WeikaiComponentSPI - The weikai_spi::WeikaiComponentSPI class implements the virtual methods of the WeikaiComponent class for an SPI bus. - - @section WeikaiGPIOPin_ WeikaiGPIOPin class - The WeikaiGPIOPin class is an helper class to expose the GPIO pins of WK family components as if they were internal - GPIO pins. It also provides the setup() and dump_summary() methods. - - @section WeikaiChannel_ The WeikaiChannel class - The WeikaiChannel class is used to implement all the virtual methods of the ESPHome uart::UARTComponent class. An - individual instance of this class is created for each UART channel. It has a link back to the WeikaiComponent object it - belongs to. This class derives from the uart::UARTComponent class. It collaborates through an aggregation with - WeikaiComponent. This implies that WeikaiComponent acts as a container, housing several WeikaiChannel instances. - Furthermore, the WeikaiChannel class derives from the ESPHome uart::UARTComponent class, it also has an association - relationship with the WKRingBuffer and WeikaiRegister helper classes. Consequently, when a WeikaiChannel instance is - destroyed, the associated WKRingBuffer instance is also destroyed. - -*/ - static const char *const TAG = "weikai"; /// @brief convert an int to binary representation as C++ std::string diff --git a/esphome/core/doxygen.h b/esphome/core/doxygen.h new file mode 100644 index 0000000000..36a394bee9 --- /dev/null +++ b/esphome/core/doxygen.h @@ -0,0 +1,13 @@ +#pragma once + +namespace esphome { + +/*! @mainpage ESPHome source code documentation + This documentation provides references to the ESPHome source code classes and methods. + + @details This documentation site is purely for reference and does not contain any user documentation. + If you are contributing to ESPHome or building an ESPHome component, then you should be starting at + https://developers.esphome.io. +*/ + +} // namespace esphome From 2e15ee232d4e577df0243f793da266d870bcdb20 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 May 2025 14:09:38 +1200 Subject: [PATCH 150/193] Deploy doxygen docs to netlify (#8837) --- .github/workflows/release.yml | 50 + .gitignore | 1 + Doxyfile | 2877 +++++++++++++++++++++++++++++++++ script/bump-version.py | 6 + 4 files changed, 2934 insertions(+) create mode 100644 Doxyfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41e9186987..098f1ec733 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ jobs: outputs: tag: ${{ steps.tag.outputs.tag }} branch_build: ${{ steps.tag.outputs.branch_build }} + deploy_env: ${{ steps.tag.outputs.environment }} steps: - uses: actions/checkout@v4.1.7 - name: Get tag @@ -27,6 +28,11 @@ jobs: if [[ "${{ github.event_name }}" = "release" ]]; then TAG="${{ github.event.release.tag_name}}" BRANCH_BUILD="false" + if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then + ENVIRONMENT="beta" + else + ENVIRONMENT="production" + fi else TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") today="$(date --utc '+%Y%m%d')" @@ -35,12 +41,15 @@ jobs: if [[ "$BRANCH" != "dev" ]]; then TAG="${TAG}-${BRANCH}" BRANCH_BUILD="true" + ENVIRONMENT="" else BRANCH_BUILD="false" + ENVIRONMENT="dev" fi fi echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT + echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT # yamllint enable rule:line-length deploy-pypi: @@ -251,3 +260,44 @@ jobs: version: "${{ needs.init.outputs.tag }}", } }) + + deploy-api-docs: + if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' + runs-on: ubuntu-latest + needs: [init] + environment: ${{ needs.init.outputs.deploy_env }} + steps: + - name: Checkout repo + uses: actions/checkout@v4.1.7 + + - name: Set up Node.js + uses: actions/setup-node@v4.4.0 + with: + node-version: "22" + + - name: Generate docs + uses: mattnotmitt/doxygen-action@v1.12.0 + + - name: Deploy to netlify ${{ needs.init.outputs.deploy_env }} + if: needs.init.outputs.deploy_env != 'production' + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + run: | + npx netlify-cli deploy \ + --dir api-docs \ + --no-build \ + --alias ${{ needs.init.outputs.deploy_env }} \ + --message "Deploy API docs for ${{ needs.init.outputs.tag }}" + + - name: Deploy to netlify production + if: needs.init.outputs.deploy_env == 'production' + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + run: | + npx netlify-cli deploy \ + --dir api-docs \ + --no-build \ + --prod \ + --message "Deploy API docs for ${{ needs.init.outputs.tag }}" diff --git a/.gitignore b/.gitignore index ad38e26fdd..390d1ab45b 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,4 @@ sdkconfig.* /components /managed_components +api-docs/ diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000000..f807ba5c4e --- /dev/null +++ b/Doxyfile @@ -0,0 +1,2877 @@ +# Doxyfile 1.12.0 + +# This file describes the settings to be used by the documentation system +# Doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use Doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use Doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = ESPHome + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 2025.6.0-dev + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where Doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = . + +# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this +# option can be useful when feeding Doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + +# If the ALLOW_UNICODE_NAMES tag is set to YES, Doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = YES + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by Doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, Doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, Doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, Doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, Doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which Doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where Doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, Doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the JAVADOC_BANNER tag is set to YES then Doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by Doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and Doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# Doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as Doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = NO + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then Doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 2 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = YES + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by Doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make Doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by Doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by Doxygen, so you can +# mix Doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 6. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 0 + +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + +# When enabled Doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let Doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also makes the inheritance and +# collaboration diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software) sources only. Doxygen will parse +# them like normal C++ but will assume all classes use public instead of private +# inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# Doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then Doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, Doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# Doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run Doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number of threads Doxygen is allowed to use +# during processing. When set to 0 Doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, Doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = NO + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = YES + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES Doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and macOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then Doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then Doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then Doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then Doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then Doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then Doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then Doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and Doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING Doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = NO + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = NO + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = NO + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= NO + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# Doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by Doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by Doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents Doxygen's defaults, run Doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run Doxygen from a directory containing a file called +# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +# The EXTERNAL_TOOL_PATH tag can be used to extend the search path (PATH +# environment variable) so that external tools such as latex and gs can be +# found. +# Note: Directories specified with EXTERNAL_TOOL_PATH are added in front of the +# path already specified by the PATH variable, and are added in the order +# specified. +# Note: This option is particularly useful for macOS version 14 (Sonoma) and +# higher, when running Doxygen from Doxywizard, because in this case any user- +# defined changes to the PATH are ignored. A typical example on macOS is to set +# EXTERNAL_TOOL_PATH = /Library/TeX/texbin /usr/local/bin +# together with the standard path, the full search path used by doxygen when +# launching external tools will then become +# PATH=/Library/TeX/texbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +EXTERNAL_TOOL_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by Doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = YES + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by Doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then Doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = NO + +# If the WARN_IF_DOC_ERROR tag is set to YES, Doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, Doxygen will warn about incomplete +# function parameter documentation. If set to NO, Doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, Doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, Doxygen will warn about +# undocumented enumeration values. If set to NO, Doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + +# If the WARN_AS_ERROR tag is set to YES then Doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then Doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the Doxygen process Doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then Doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined Doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that Doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of Doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = esphome + +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING) if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). +# See also: INPUT_ENCODING for further information on supported encodings. + +INPUT_FILE_ENCODING = + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by Doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cpp \ + *.tcc \ + *.h + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which Doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# ANamespace::AClass, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = *.cpp + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = YES + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that Doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that Doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by Doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by Doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the Doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# multi-line macros, enums or list initialized variables directly into the +# documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct Doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of Doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by Doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then Doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then Doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which Doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then Doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by Doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, Doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = api-docs + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank Doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that Doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that Doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of Doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank Doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that Doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank Doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that Doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by Doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generates light mode output, DARK always +# generates dark mode output, AUTO_LIGHT automatically sets the mode according +# to the user preference, uses light mode if no preference is set (the default), +# AUTO_DARK automatically sets the mode according to the user preference, uses +# dark mode if no preference is set and TOGGLE allows a user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = AUTO_LIGHT + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then Doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# Doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, Doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then Doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# Doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty Doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by Doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# Doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# When the SHOW_ENUM_VALUES tag is set doxygen will show the specified +# enumeration values besides the enumeration mnemonics. +# The default value is: NO. + +SHOW_ENUM_VALUES = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, Doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the OBFUSCATE_EMAILS tag is set to YES, Doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, Doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# Doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for MathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/ + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled Doxygen will generate a search box for +# the HTML output. The underlying search engine uses JavaScript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the JavaScript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /